also looking at this
feat: native trail maps and complete native mobile screens
#1017
60 files
+2505
-823
| @@ -1,250 +1,410 @@ | ||
| 1 | 1 | api: |
| 2 | - 'storage/framework/api/**' | |
| 2 | - changed-files: | |
| 3 | - any-glob-to-any-file: | |
| 4 | - "storage/framework/api/**" | |
| 3 | 5 | |
| 4 | 6 | deps: |
| 5 | - ./package.json | |
| 7 | - changed-files: | |
| 8 | - any-glob-to-any-file: | |
| 9 | - "package.json" | |
| 6 | 10 | |
| 7 | 11 | core: |
| 8 | - 'storage/framework/core/**' | |
| 12 | - changed-files: | |
| 13 | - any-glob-to-any-file: | |
| 14 | - "storage/framework/core/**" | |
| 9 | 15 | |
| 10 | 16 | ide: |
| 11 | - 'storage/framework/defaults/ide/**' | |
| 17 | - changed-files: | |
| 18 | - any-glob-to-any-file: | |
| 19 | - "storage/framework/defaults/ide/**" | |
| 12 | 20 | |
| 13 | 21 | jetbrains: |
| 14 | - 'storage/framework/defaults/ide/jetbrains/**' | |
| 22 | - changed-files: | |
| 23 | - any-glob-to-any-file: | |
| 24 | - "storage/framework/defaults/ide/jetbrains/**" | |
| 15 | 25 | |
| 16 | 26 | vscode: |
| 17 | - 'storage/framework/defaults/ide/vscode/**' | |
| 27 | - changed-files: | |
| 28 | - any-glob-to-any-file: | |
| 29 | - "storage/framework/defaults/ide/vscode/**" | |
| 18 | 30 | |
| 19 | 31 | libs: |
| 20 | - 'storage/framework/libs/**' | |
| 32 | - changed-files: | |
| 33 | - any-glob-to-any-file: | |
| 34 | - "storage/framework/libs/**" | |
| 21 | 35 | |
| 22 | 36 | components: |
| 23 | - 'storage/framework/libs/components/**' | |
| 24 | - resources/components/** | |
| 37 | - changed-files: | |
| 38 | - any-glob-to-any-file: | |
| 39 | - "storage/framework/libs/components/**" | |
| 40 | - "resources/components/**" | |
| 25 | 41 | |
| 26 | 42 | entries: |
| 27 | - storage/framework/libs/entries/** | |
| 43 | - changed-files: | |
| 44 | - any-glob-to-any-file: | |
| 45 | - "storage/framework/libs/entries/**" | |
| 28 | 46 | |
| 29 | 47 | examples: |
| 30 | - storage/framework/libs/examples/** | |
| 48 | - changed-files: | |
| 49 | - any-glob-to-any-file: | |
| 50 | - "storage/framework/libs/examples/**" | |
| 31 | 51 | |
| 32 | 52 | functions: |
| 33 | - 'storage/framework/libs/functions/**' | |
| 34 | - resources/functions/** | |
| 53 | - changed-files: | |
| 54 | - any-glob-to-any-file: | |
| 55 | - "storage/framework/libs/functions/**" | |
| 56 | - "resources/functions/**" | |
| 35 | 57 | |
| 36 | 58 | scripts: |
| 37 | - ./storage/framework/scripts/** | |
| 59 | - changed-files: | |
| 60 | - any-glob-to-any-file: | |
| 61 | - "storage/framework/scripts/**" | |
| 38 | 62 | |
| 39 | 63 | stack: |
| 40 | - 'storage/framework/stacks/**' | |
| 64 | - changed-files: | |
| 65 | - any-glob-to-any-file: | |
| 66 | - "storage/framework/stacks/**" | |
| 41 | 67 | |
| 42 | 68 | dashboard: |
| 43 | - 'storage/framework/stacks/dashboard/**' | |
| 69 | - changed-files: | |
| 70 | - any-glob-to-any-file: | |
| 71 | - "storage/framework/stacks/dashboard/**" | |
| 44 | 72 | |
| 45 | 73 | vcs: |
| 46 | - ./storage/framework/core/vcs/** | |
| 74 | - changed-files: | |
| 75 | - any-glob-to-any-file: | |
| 76 | - "storage/framework/core/vcs/**" | |
| 47 | 77 | |
| 48 | 78 | github: |
| 49 | - ./storage/framework/core/vcs/github/** | |
| 79 | - changed-files: | |
| 80 | - any-glob-to-any-file: | |
| 81 | - "storage/framework/core/vcs/github/**" | |
| 50 | 82 | |
| 51 | 83 | views: |
| 52 | - 'storage/framework/core/views/**' | |
| 53 | - resources/views/** | |
| 84 | - changed-files: | |
| 85 | - any-glob-to-any-file: | |
| 86 | - "storage/framework/core/views/**" | |
| 87 | - "resources/views/**" | |
| 54 | 88 | |
| 55 | 89 | web: |
| 56 | - ./storage/framework/core/views/web/** | |
| 90 | - changed-files: | |
| 91 | - any-glob-to-any-file: | |
| 92 | - "storage/framework/core/views/web/**" | |
| 57 | 93 | |
| 58 | 94 | actions: |
| 59 | - 'storage/framework/core/actions/**' | |
| 95 | - changed-files: | |
| 96 | - any-glob-to-any-file: | |
| 97 | - "storage/framework/core/actions/**" | |
| 60 | 98 | |
| 61 | 99 | ai: |
| 62 | - 'storage/framework/core/ai/**' | |
| 100 | - changed-files: | |
| 101 | - any-glob-to-any-file: | |
| 102 | - "storage/framework/core/ai/**" | |
| 63 | 103 | |
| 64 | 104 | alias: |
| 65 | - 'storage/framework/core/alias/**' | |
| 105 | - changed-files: | |
| 106 | - any-glob-to-any-file: | |
| 107 | - "storage/framework/core/alias/**" | |
| 66 | 108 | |
| 67 | 109 | analytics: |
| 68 | - 'storage/framework/core/analytics/**' | |
| 110 | - changed-files: | |
| 111 | - any-glob-to-any-file: | |
| 112 | - "storage/framework/core/analytics/**" | |
| 69 | 113 | |
| 70 | 114 | arrays: |
| 71 | - 'storage/framework/core/utils/arrays/**' | |
| 115 | - changed-files: | |
| 116 | - any-glob-to-any-file: | |
| 117 | - "storage/framework/core/utils/arrays/**" | |
| 72 | 118 | |
| 73 | 119 | auth: |
| 74 | - 'storage/framework/core/auth/**' | |
| 120 | - changed-files: | |
| 121 | - any-glob-to-any-file: | |
| 122 | - "storage/framework/core/auth/**" | |
| 75 | 123 | |
| 76 | 124 | buddy: |
| 77 | - 'storage/framework/core/buddy/**' | |
| 125 | - changed-files: | |
| 126 | - any-glob-to-any-file: | |
| 127 | - "storage/framework/core/buddy/**" | |
| 78 | 128 | |
| 79 | 129 | build: |
| 80 | - 'storage/framework/core/build/**' | |
| 130 | - changed-files: | |
| 131 | - any-glob-to-any-file: | |
| 132 | - "storage/framework/core/build/**" | |
| 81 | 133 | |
| 82 | 134 | cache: |
| 83 | - 'storage/framework/core/cache/**' | |
| 135 | - changed-files: | |
| 136 | - any-glob-to-any-file: | |
| 137 | - "storage/framework/core/cache/**" | |
| 84 | 138 | |
| 85 | 139 | chat: |
| 86 | - 'storage/framework/core/notifications/chat/**' | |
| 140 | - changed-files: | |
| 141 | - any-glob-to-any-file: | |
| 142 | - "storage/framework/core/notifications/chat/**" | |
| 87 | 143 | |
| 88 | 144 | cli: |
| 89 | - 'storage/framework/core/cli/**' | |
| 90 | - app/Commands/** | |
| 145 | - changed-files: | |
| 146 | - any-glob-to-any-file: | |
| 147 | - "storage/framework/core/cli/**" | |
| 148 | - "app/Commands/**" | |
| 91 | 149 | |
| 92 | 150 | cloud: |
| 93 | - 'storage/framework/core/cloud/**' | |
| 151 | - changed-files: | |
| 152 | - any-glob-to-any-file: | |
| 153 | - "storage/framework/core/cloud/**" | |
| 94 | 154 | |
| 95 | 155 | collections: |
| 96 | - 'storage/framework/core/utils/collections/**' | |
| 156 | - changed-files: | |
| 157 | - any-glob-to-any-file: | |
| 158 | - "storage/framework/core/utils/collections/**" | |
| 97 | 159 | |
| 98 | 160 | config: |
| 99 | - 'storage/framework/core/config/**' | |
| 100 | - config/** | |
| 161 | - changed-files: | |
| 162 | - any-glob-to-any-file: | |
| 163 | - "storage/framework/core/config/**" | |
| 164 | - "config/**" | |
| 101 | 165 | |
| 102 | 166 | database: |
| 103 | - 'storage/framework/core/database/**' | |
| 167 | - changed-files: | |
| 168 | - any-glob-to-any-file: | |
| 169 | - "storage/framework/core/database/**" | |
| 104 | 170 | |
| 105 | 171 | datetime: |
| 106 | - 'storage/framework/core/utils/datetime/**' | |
| 172 | - changed-files: | |
| 173 | - any-glob-to-any-file: | |
| 174 | - "storage/framework/core/utils/datetime/**" | |
| 107 | 175 | |
| 108 | 176 | desktop: |
| 109 | - 'storage/framework/core/desktop/**' | |
| 110 | - ./storage/framework/core/views/desktop/** | |
| 177 | - changed-files: | |
| 178 | - any-glob-to-any-file: | |
| 179 | - "storage/framework/core/desktop/**" | |
| 180 | - "storage/framework/core/views/desktop/**" | |
| 111 | 181 | |
| 112 | 182 | development: |
| 113 | - 'storage/framework/core/development/**' | |
| 183 | - changed-files: | |
| 184 | - any-glob-to-any-file: | |
| 185 | - "storage/framework/core/development/**" | |
| 114 | 186 | |
| 115 | 187 | dns: |
| 116 | - 'storage/framework/core/dns/**' | |
| 188 | - changed-files: | |
| 189 | - any-glob-to-any-file: | |
| 190 | - "storage/framework/core/dns/**" | |
| 117 | 191 | |
| 118 | 192 | docs: |
| 119 | - 'storage/framework/core/docs/**' | |
| 120 | - 'storage/framework/docs/**' | |
| 121 | - docs/** | |
| 122 | - config/docs.ts | |
| 193 | - changed-files: | |
| 194 | - any-glob-to-any-file: | |
| 195 | - "storage/framework/core/docs/**" | |
| 196 | - "storage/framework/docs/**" | |
| 197 | - "docs/**" | |
| 198 | - "config/docs.ts" | |
| 123 | 199 | |
| 124 | 200 | email: |
| 125 | - 'storage/framework/core/notifications/email/**' | |
| 201 | - changed-files: | |
| 202 | - any-glob-to-any-file: | |
| 203 | - "storage/framework/core/notifications/email/**" | |
| 126 | 204 | |
| 127 | 205 | error-handling: |
| 128 | - 'storage/framework/core/error-handling/**' | |
| 206 | - changed-files: | |
| 207 | - any-glob-to-any-file: | |
| 208 | - "storage/framework/core/error-handling/**" | |
| 129 | 209 | |
| 130 | 210 | events: |
| 131 | - 'storage/framework/core/events/**' | |
| 132 | - app/Events/** | |
| 211 | - changed-files: | |
| 212 | - any-glob-to-any-file: | |
| 213 | - "storage/framework/core/events/**" | |
| 214 | - "app/Events/**" | |
| 133 | 215 | |
| 134 | 216 | faker: |
| 135 | - 'storage/framework/core/faker/**' | |
| 217 | - changed-files: | |
| 218 | - any-glob-to-any-file: | |
| 219 | - "storage/framework/core/faker/**" | |
| 136 | 220 | |
| 137 | 221 | git: |
| 138 | - 'storage/framework/core/utils/git/**' | |
| 222 | - changed-files: | |
| 223 | - any-glob-to-any-file: | |
| 224 | - "storage/framework/core/utils/git/**" | |
| 139 | 225 | |
| 140 | 226 | health: |
| 141 | - 'storage/framework/core/health/**' | |
| 227 | - changed-files: | |
| 228 | - any-glob-to-any-file: | |
| 229 | - "storage/framework/core/health/**" | |
| 142 | 230 | |
| 143 | 231 | lint: |
| 144 | - 'storage/framework/core/lint/**' | |
| 232 | - changed-files: | |
| 233 | - any-glob-to-any-file: | |
| 234 | - "storage/framework/core/lint/**" | |
| 145 | 235 | |
| 146 | 236 | logging: |
| 147 | - 'storage/framework/core/logging/**' | |
| 237 | - changed-files: | |
| 238 | - any-glob-to-any-file: | |
| 239 | - "storage/framework/core/logging/**" | |
| 148 | 240 | |
| 149 | 241 | modules: |
| 150 | - 'storage/framework/core/modules/**' | |
| 242 | - changed-files: | |
| 243 | - any-glob-to-any-file: | |
| 244 | - "storage/framework/core/modules/**" | |
| 151 | 245 | |
| 152 | 246 | notifications: |
| 153 | - 'storage/framework/core/notifications/**' | |
| 154 | - app/Notifications/** | |
| 247 | - changed-files: | |
| 248 | - any-glob-to-any-file: | |
| 249 | - "storage/framework/core/notifications/**" | |
| 250 | - "app/Notifications/**" | |
| 155 | 251 | |
| 156 | 252 | objects: |
| 157 | - 'storage/framework/core/utils/objects/**' | |
| 253 | - changed-files: | |
| 254 | - any-glob-to-any-file: | |
| 255 | - "storage/framework/core/utils/objects/**" | |
| 158 | 256 | |
| 159 | 257 | orm: |
| 160 | - 'storage/framework/core/orm/**' | |
| 258 | - changed-files: | |
| 259 | - any-glob-to-any-file: | |
| 260 | - "storage/framework/core/orm/**" | |
| 161 | 261 | |
| 162 | 262 | path: |
| 163 | - 'storage/framework/core/path/**' | |
| 263 | - changed-files: | |
| 264 | - any-glob-to-any-file: | |
| 265 | - "storage/framework/core/path/**" | |
| 164 | 266 | |
| 165 | 267 | payments: |
| 166 | - 'storage/framework/core/payments/**' | |
| 268 | - changed-files: | |
| 269 | - any-glob-to-any-file: | |
| 270 | - "storage/framework/core/payments/**" | |
| 167 | 271 | |
| 168 | 272 | push: |
| 169 | - 'storage/framework/core/notifications/push/**' | |
| 273 | - changed-files: | |
| 274 | - any-glob-to-any-file: | |
| 275 | - "storage/framework/core/notifications/push/**" | |
| 170 | 276 | |
| 171 | 277 | query-builder: |
| 172 | - 'storage/framework/core/query-builder/**' | |
| 278 | - changed-files: | |
| 279 | - any-glob-to-any-file: | |
| 280 | - "storage/framework/core/query-builder/**" | |
| 173 | 281 | |
| 174 | 282 | queue: |
| 175 | - 'storage/framework/core/queue/**' | |
| 283 | - changed-files: | |
| 284 | - any-glob-to-any-file: | |
| 285 | - "storage/framework/core/queue/**" | |
| 176 | 286 | |
| 177 | 287 | real-time: |
| 178 | - 'storage/framework/core/real-time/**' | |
| 288 | - changed-files: | |
| 289 | - any-glob-to-any-file: | |
| 290 | - "storage/framework/core/real-time/**" | |
| 179 | 291 | |
| 180 | 292 | repl: |
| 181 | - 'storage/framework/core/repl/**' | |
| 293 | - changed-files: | |
| 294 | - any-glob-to-any-file: | |
| 295 | - "storage/framework/core/repl/**" | |
| 182 | 296 | |
| 183 | 297 | router: |
| 184 | - 'storage/framework/core/router/**' | |
| 298 | - changed-files: | |
| 299 | - any-glob-to-any-file: | |
| 300 | - "storage/framework/core/router/**" | |
| 185 | 301 | |
| 186 | 302 | scheduler: |
| 187 | - 'storage/framework/core/scheduler/**' | |
| 303 | - changed-files: | |
| 304 | - any-glob-to-any-file: | |
| 305 | - "storage/framework/core/scheduler/**" | |
| 188 | 306 | |
| 189 | 307 | search-engine: |
| 190 | - 'storage/framework/core/search-engine/**' | |
| 308 | - changed-files: | |
| 309 | - any-glob-to-any-file: | |
| 310 | - "storage/framework/core/search-engine/**" | |
| 191 | 311 | |
| 192 | 312 | security: |
| 193 | - 'storage/framework/core/security/**' | |
| 313 | - changed-files: | |
| 314 | - any-glob-to-any-file: | |
| 315 | - "storage/framework/core/security/**" | |
| 194 | 316 | |
| 195 | 317 | signals: |
| 196 | - 'storage/framework/core/signals/**' | |
| 318 | - changed-files: | |
| 319 | - any-glob-to-any-file: | |
| 320 | - "storage/framework/core/signals/**" | |
| 197 | 321 | |
| 198 | 322 | slug: |
| 199 | - 'storage/framework/core/slug/**' | |
| 323 | - changed-files: | |
| 324 | - any-glob-to-any-file: | |
| 325 | - "storage/framework/core/slug/**" | |
| 200 | 326 | |
| 201 | 327 | sms: |
| 202 | - 'storage/framework/core/notifications/sms/**' | |
| 328 | - changed-files: | |
| 329 | - any-glob-to-any-file: | |
| 330 | - "storage/framework/core/notifications/sms/**" | |
| 203 | 331 | |
| 204 | 332 | storage: |
| 205 | - storage/** | |
| 333 | - changed-files: | |
| 334 | - any-glob-to-any-file: | |
| 335 | - "storage/**" | |
| 206 | 336 | |
| 207 | 337 | strings: |
| 208 | - 'storage/framework/core/utils/strings/**' | |
| 338 | - changed-files: | |
| 339 | - any-glob-to-any-file: | |
| 340 | - "storage/framework/core/utils/strings/**" | |
| 209 | 341 | |
| 210 | 342 | testing: |
| 211 | - 'storage/framework/core/testing/**' | |
| 343 | - changed-files: | |
| 344 | - any-glob-to-any-file: | |
| 345 | - "storage/framework/core/testing/**" | |
| 212 | 346 | |
| 213 | 347 | tinker: |
| 214 | - 'storage/framework/core/tinker/**' | |
| 348 | - changed-files: | |
| 349 | - any-glob-to-any-file: | |
| 350 | - "storage/framework/core/tinker/**" | |
| 215 | 351 | |
| 216 | 352 | types: |
| 217 | - 'storage/framework/core/types/**' | |
| 353 | - changed-files: | |
| 354 | - any-glob-to-any-file: | |
| 355 | - "storage/framework/core/types/**" | |
| 218 | 356 | |
| 219 | 357 | ui: |
| 220 | - 'storage/framework/core/ui/**' | |
| 358 | - changed-files: | |
| 359 | - any-glob-to-any-file: | |
| 360 | - "storage/framework/core/ui/**" | |
| 221 | 361 | |
| 222 | 362 | utils: |
| 223 | - 'storage/framework/core/utils/**' | |
| 363 | - changed-files: | |
| 364 | - any-glob-to-any-file: | |
| 365 | - "storage/framework/core/utils/**" | |
| 224 | 366 | |
| 225 | 367 | validation: |
| 226 | - 'storage/framework/core/validation/**' | |
| 368 | - changed-files: | |
| 369 | - any-glob-to-any-file: | |
| 370 | - "storage/framework/core/validation/**" | |
| 227 | 371 | |
| 228 | 372 | app: |
| 229 | - app/** | |
| 373 | - changed-files: | |
| 374 | - any-glob-to-any-file: | |
| 375 | - "app/**" | |
| 230 | 376 | |
| 231 | 377 | jobs: |
| 232 | - app/Jobs/** | |
| 378 | - changed-files: | |
| 379 | - any-glob-to-any-file: | |
| 380 | - "app/Jobs/**" | |
| 233 | 381 | |
| 234 | 382 | models: |
| 235 | - app/Models/** | |
| 383 | - changed-files: | |
| 384 | - any-glob-to-any-file: | |
| 385 | - "app/Models/**" | |
| 236 | 386 | |
| 237 | 387 | resources: |
| 238 | - resources/** | |
| 388 | - changed-files: | |
| 389 | - any-glob-to-any-file: | |
| 390 | - "resources/**" | |
| 239 | 391 | |
| 240 | 392 | lang: |
| 241 | - resources/lang/** | |
| 393 | - changed-files: | |
| 394 | - any-glob-to-any-file: | |
| 395 | - "resources/lang/**" | |
| 242 | 396 | |
| 243 | 397 | stores: |
| 244 | - resources/stores/** | |
| 398 | - changed-files: | |
| 399 | - any-glob-to-any-file: | |
| 400 | - "resources/stores/**" | |
| 245 | 401 | |
| 246 | 402 | routes: |
| 247 | - routes/** | |
| 403 | - changed-files: | |
| 404 | - any-glob-to-any-file: | |
| 405 | - "routes/**" | |
| 248 | 406 | |
| 249 | 407 | tests: |
| 250 | - tests/** | |
| 408 | - changed-files: | |
| 409 | - any-glob-to-any-file: | |
| 410 | - "tests/**" | |
| @@ -48,7 +48,7 @@ jobs: | ||
| 48 | 48 | node-modules- |
| 49 | 49 | |
| 50 | 50 | - name: Install dependencies |
| 51 | run: bun install | |
| 51 | run: bun install --frozen-lockfile | |
| 52 | 52 | |
| 53 | 53 | - name: Lint |
| 54 | 54 | run: bunx --bun pickier . |
| @@ -84,7 +84,7 @@ jobs: | ||
| 84 | 84 | node-modules- |
| 85 | 85 | |
| 86 | 86 | - name: Install dependencies |
| 87 | run: bun install | |
| 87 | run: bun install --frozen-lockfile | |
| 88 | 88 | |
| 89 | 89 | - name: Typecheck |
| 90 | 90 | run: bun buddy test:types |
| @@ -146,12 +146,12 @@ jobs: | ||
| 146 | 146 | node-modules- |
| 147 | 147 | |
| 148 | 148 | - name: Install Dependencies |
| 149 | run: bun install | |
| 149 | run: bun install --frozen-lockfile | |
| 150 | 150 | |
| 151 | 151 | - name: Test Suite |
| 152 | 152 | env: |
| 153 | 153 | AWS_DYNAMODB_ENDPOINT: http://localhost:8000 |
| 154 | run: bun test | |
| 154 | run: ./buddy test | |
| 155 | 155 | |
| 156 | 156 | recording-qa: |
| 157 | 157 | runs-on: ubuntu-latest |
| @@ -165,7 +165,7 @@ jobs: | ||
| 165 | 165 | # Production's Bun, as in the jobs above. |
| 166 | 166 | bun-version: '1.4.2' |
| 167 | 167 | - name: Install app dependencies |
| 168 | run: bun install | |
| 168 | run: bun install --frozen-lockfile | |
| 169 | 169 | # Isolated test dependencies: no changes to the app's hoisted ORM tree. |
| 170 | 170 | # Server-side regressions that need the QA app but no browser. |
| 171 | 171 | # |
| @@ -243,7 +243,7 @@ jobs: | ||
| 243 | 243 | node-modules- |
| 244 | 244 | |
| 245 | 245 | - name: Install Dependencies |
| 246 | run: bun install | |
| 246 | run: bun install --frozen-lockfile | |
| 247 | 247 | |
| 248 | 248 | - name: Build |
| 249 | 249 | run: bun run build |
| @@ -255,20 +255,11 @@ jobs: | ||
| 255 | 255 | # `"private": true` because it is an application rather than a library, |
| 256 | 256 | # so it answered `400 {"message":"No packages"}` on every run. |
| 257 | 257 | # |
| 258 | # The installer action lives inside the pantry monorepo (packages/action) | |
| 259 | # and is not published as `pantry-pm/pantry-installer` yet, so the | |
| 260 | # release binary is fetched directly. Swap this step for the action once | |
| 261 | # that repo exists. | |
| 262 | 258 | - name: Install Pantry |
| 263 | run: | | |
| 264 | tmp=$(mktemp -d) | |
| 265 | curl -fsSL -o "$tmp/pantry.zip" \ | |
| 266 | https://github.com/pantry-pm/pantry/releases/latest/download/pantry-linux-x64.zip | |
| 267 | # The archive holds a bare `pantry` binary, no wrapping directory. | |
| 268 | unzip -oq "$tmp/pantry.zip" -d "$tmp" | |
| 269 | sudo install -m 755 "$tmp/pantry" /usr/local/bin/pantry | |
| 270 | rm -rf "$tmp" | |
| 271 | pantry --version | |
| 259 | uses: pantry-pm/pantry/packages/action@da17bb9317e8f8c247618fb176fe97269f15bfe4 # v0.11.94 | |
| 260 | with: | |
| 261 | version: '0.11.94' | |
| 262 | install: 'false' | |
| 272 | 263 | |
| 273 | 264 | # `publish:commit` derives the version from the commit SHA itself, so |
| 274 | 265 | # each build is separately installable without anyone stamping |
| @@ -96,7 +96,10 @@ jobs: | ||
| 96 | 96 | bun-version: '1.4.2' |
| 97 | 97 | |
| 98 | 98 | - name: Setup Pantry |
| 99 | uses: home-lang/pantry/packages/action@main | |
| 99 | uses: pantry-pm/pantry/packages/action@da17bb9317e8f8c247618fb176fe97269f15bfe4 # v0.11.94 | |
| 100 | with: | |
| 101 | version: '0.11.94' | |
| 102 | install: 'false' | |
| 100 | 103 | |
| 101 | 104 | - name: Install dependencies |
| 102 | 105 | run: bun install --frozen-lockfile |
| @@ -8,6 +8,8 @@ on: | ||
| 8 | 8 | - '.maestro/**' |
| 9 | 9 | - 'tests/native/**' |
| 10 | 10 | - 'scripts/run-native-trail-e2e.ts' |
| 11 | - 'scripts/run-native-trail-android-e2e.ts' | |
| 12 | - 'scripts/run-owned-android-e2e.ts' | |
| 11 | 13 | - 'app/**' |
| 12 | 14 | - 'config/mobile.ts' |
| 13 | 15 | # The app icon config/mobile.ts points at; a bad one fails the iOS build. |
| @@ -21,6 +23,8 @@ on: | ||
| 21 | 23 | - '.maestro/**' |
| 22 | 24 | - 'tests/native/**' |
| 23 | 25 | - 'scripts/run-native-trail-e2e.ts' |
| 26 | - 'scripts/run-native-trail-android-e2e.ts' | |
| 27 | - 'scripts/run-owned-android-e2e.ts' | |
| 24 | 28 | - 'app/**' |
| 25 | 29 | - 'config/mobile.ts' |
| 26 | 30 | # The app icon config/mobile.ts points at; a bad one fails the iOS build. |
| @@ -53,7 +57,10 @@ jobs: | ||
| 53 | 57 | distribution: temurin |
| 54 | 58 | java-version: '17' |
| 55 | 59 | - name: Setup Pantry |
| 56 | uses: home-lang/pantry/packages/action@main | |
| 60 | uses: pantry-pm/pantry/packages/action@da17bb9317e8f8c247618fb176fe97269f15bfe4 # v0.11.94 | |
| 61 | with: | |
| 62 | version: '0.11.94' | |
| 63 | install: 'false' | |
| 57 | 64 | - name: Install Maestro |
| 58 | 65 | run: | |
| 59 | 66 | curl -fsSL "https://github.com/mobile-dev-inc/Maestro/releases/download/${MAESTRO_VERSION}/maestro.zip" -o "$RUNNER_TEMP/maestro.zip" |
| @@ -75,8 +82,6 @@ jobs: | ||
| 75 | 82 | if-no-files-found: warn |
| 76 | 83 | |
| 77 | 84 | android: |
| 78 | env: | |
| 79 | ANDROID_MAPS_API_KEY: ${{ secrets.ANDROID_MAPS_API_KEY }} | |
| 80 | 85 | runs-on: ubuntu-latest |
| 81 | 86 | timeout-minutes: 30 |
| 82 | 87 | steps: |
| @@ -91,6 +96,11 @@ jobs: | ||
| 91 | 96 | - uses: gradle/actions/setup-gradle@v4 |
| 92 | 97 | with: |
| 93 | 98 | gradle-version: '8.11.1' |
| 99 | - uses: android-actions/setup-android@v4 | |
| 100 | with: | |
| 101 | packages: platform-tools | |
| 102 | - name: Install the published SDK compile platform | |
| 103 | run: sdkmanager "platforms;android-36" "build-tools;36.0.0" | |
| 94 | 104 | - name: Enable KVM |
| 95 | 105 | run: | |
| 96 | 106 | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules |
| @@ -107,15 +117,7 @@ jobs: | ||
| 107 | 117 | - name: Build Android E2E app |
| 108 | 118 | run: bun scripts/run-mobile-e2e.ts android --build-only |
| 109 | 119 | - name: Run Android E2E flows |
| 110 | uses: reactivecircus/android-emulator-runner@v2 | |
| 111 | with: | |
| 112 | api-level: 34 | |
| 113 | target: google_apis | |
| 114 | arch: x86_64 | |
| 115 | profile: pixel_7 | |
| 116 | ram-size: 4096M | |
| 117 | disable-animations: true | |
| 118 | script: bun scripts/run-mobile-e2e.ts android --skip-build | |
| 120 | run: bun scripts/run-owned-android-e2e.ts baseline | |
| 119 | 121 | - name: Upload Android diagnostics |
| 120 | 122 | if: always() |
| 121 | 123 | uses: actions/upload-artifact@v5 |
| @@ -125,17 +127,25 @@ jobs: | ||
| 125 | 127 | if-no-files-found: warn |
| 126 | 128 | |
| 127 | 129 | native-trail-ios: |
| 130 | strategy: | |
| 131 | fail-fast: false | |
| 132 | matrix: | |
| 133 | group: [maps, reviews, utilities, navigation] | |
| 128 | 134 | runs-on: macos-latest |
| 129 | 135 | timeout-minutes: 35 |
| 130 | 136 | env: |
| 131 | 137 | QA_PORT_BASE: '6320' |
| 138 | NATIVE_TRAIL_GROUP: ${{ matrix.group }} | |
| 132 | 139 | steps: |
| 133 | 140 | - uses: actions/checkout@v5 |
| 134 | 141 | - uses: oven-sh/setup-bun@v2 |
| 135 | 142 | with: |
| 136 | 143 | bun-version: '1.4.2' |
| 137 | 144 | - name: Setup Pantry |
| 138 | uses: home-lang/pantry/packages/action@main | |
| 145 | uses: pantry-pm/pantry/packages/action@da17bb9317e8f8c247618fb176fe97269f15bfe4 # v0.11.94 | |
| 146 | with: | |
| 147 | version: '0.11.94' | |
| 148 | install: 'false' | |
| 139 | 149 | - name: Install dependencies |
| 140 | 150 | run: bun install --frozen-lockfile |
| 141 | 151 | - name: Install iOS toolchain |
| @@ -168,10 +178,58 @@ jobs: | ||
| 168 | 178 | if: always() |
| 169 | 179 | uses: actions/upload-artifact@v5 |
| 170 | 180 | with: |
| 171 | name: native-trail-ios | |
| 181 | name: native-trail-ios-${{ matrix.group }} | |
| 172 | 182 | path: | |
| 173 | 183 | storage/framework/runtime/e2e/native-trail/*.log |
| 174 | 184 | storage/framework/runtime/e2e/native-trail/*.txt |
| 185 | storage/framework/runtime/e2e/native-trail/*-persistence.json | |
| 175 | 186 | storage/framework/runtime/e2e/native-trail/*.png |
| 176 | 187 | storage/framework/runtime/e2e/native-trail/results-*.xcresult |
| 177 | 188 | if-no-files-found: warn |
| 189 | ||
| 190 | native-trail-android: | |
| 191 | runs-on: ubuntu-latest | |
| 192 | timeout-minutes: 35 | |
| 193 | env: | |
| 194 | QA_PORT_BASE: '7320' | |
| 195 | steps: | |
| 196 | - uses: actions/checkout@v5 | |
| 197 | - uses: oven-sh/setup-bun@v2 | |
| 198 | with: | |
| 199 | bun-version: '1.4.2' | |
| 200 | - uses: actions/setup-java@v5 | |
| 201 | with: | |
| 202 | distribution: temurin | |
| 203 | java-version: '17' | |
| 204 | - uses: gradle/actions/setup-gradle@v4 | |
| 205 | with: | |
| 206 | gradle-version: '8.11.1' | |
| 207 | - uses: android-actions/setup-android@v4 | |
| 208 | with: | |
| 209 | packages: platform-tools | |
| 210 | - name: Install the published SDK compile platform | |
| 211 | run: sdkmanager "platforms;android-36" "build-tools;36.0.0" | |
| 212 | - name: Install Maestro | |
| 213 | run: | | |
| 214 | curl -fsSL "https://github.com/mobile-dev-inc/Maestro/releases/download/${MAESTRO_VERSION}/maestro.zip" -o "$RUNNER_TEMP/maestro.zip" | |
| 215 | echo "$MAESTRO_SHA256 $RUNNER_TEMP/maestro.zip" | sha256sum --check | |
| 216 | unzip -q "$RUNNER_TEMP/maestro.zip" -d "$RUNNER_TEMP/maestro-cli" | |
| 217 | echo "$RUNNER_TEMP/maestro-cli/maestro/bin" >> "$GITHUB_PATH" | |
| 218 | - name: Install published dependencies | |
| 219 | run: bun install --frozen-lockfile | |
| 220 | - name: Build native MapLibre trail app | |
| 221 | run: bun scripts/run-native-trail-android-e2e.ts --build-only | |
| 222 | - name: Enable KVM | |
| 223 | run: | | |
| 224 | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules | |
| 225 | sudo udevadm control --reload-rules | |
| 226 | sudo udevadm trigger --name-match=kvm | |
| 227 | - name: Test native maps, sticky actions and persisted reviews | |
| 228 | run: bun scripts/run-owned-android-e2e.ts native-trail | |
| 229 | - name: Upload Android native trail evidence | |
| 230 | if: always() | |
| 231 | uses: actions/upload-artifact@v5 | |
| 232 | with: | |
| 233 | name: native-trail-android | |
| 234 | path: storage/framework/runtime/e2e/native-trail-android | |
| 235 | if-no-files-found: warn | |
| @@ -32,7 +32,6 @@ jobs: | ||
| 32 | 32 | source-projects: |
| 33 | 33 | env: |
| 34 | 34 | ANDROID_GOOGLE_SERVICES_JSON: ${{ secrets.ANDROID_GOOGLE_SERVICES_JSON }} |
| 35 | ANDROID_MAPS_API_KEY: ${{ secrets.ANDROID_MAPS_API_KEY }} | |
| 36 | 35 | strategy: |
| 37 | 36 | fail-fast: false |
| 38 | 37 | matrix: |
| @@ -56,9 +55,19 @@ jobs: | ||
| 56 | 55 | if: matrix.platform == 'android' |
| 57 | 56 | with: |
| 58 | 57 | gradle-version: '8.11.1' |
| 58 | - uses: android-actions/setup-android@v4 | |
| 59 | if: matrix.platform == 'android' | |
| 60 | with: | |
| 61 | packages: platform-tools | |
| 62 | - name: Install the published SDK compile platform | |
| 63 | if: matrix.platform == 'android' | |
| 64 | run: sdkmanager "platforms;android-36" "build-tools;36.0.0" | |
| 59 | 65 | - name: Setup Pantry |
| 60 | 66 | if: matrix.platform == 'ios' |
| 61 | uses: home-lang/pantry/packages/action@main | |
| 67 | uses: pantry-pm/pantry/packages/action@da17bb9317e8f8c247618fb176fe97269f15bfe4 # v0.11.94 | |
| 68 | with: | |
| 69 | version: '0.11.94' | |
| 70 | install: 'false' | |
| 62 | 71 | - name: Install dependencies |
| 63 | 72 | run: bun install --frozen-lockfile |
| 64 | 73 | - name: Install iOS toolchain |
| @@ -0,0 +1,43 @@ | ||
| 1 | appId: ${APP_ID} | |
| 2 | name: Native trail maps and sticky controls in system dark appearance | |
| 3 | --- | |
| 4 | - launchApp | |
| 5 | - openLink: wildloop://trail/${TRAIL_ID} | |
| 6 | - extendedWaitUntil: | |
| 7 | visible: ${TRAIL_NAME} | |
| 8 | timeout: 30000 | |
| 9 | - assertVisible: | |
| 10 | id: trail-sticky-actions | |
| 11 | - takeScreenshot: android-native-dark-trail-overview | |
| 12 | - scrollUntilVisible: | |
| 13 | element: | |
| 14 | id: trail-profile-toggle | |
| 15 | direction: DOWN | |
| 16 | - takeScreenshot: android-native-dark-inline-profile | |
| 17 | - scrollUntilVisible: | |
| 18 | element: | |
| 19 | id: trail-map-fullscreen | |
| 20 | direction: UP | |
| 21 | - tapOn: | |
| 22 | id: trail-map-fullscreen | |
| 23 | - extendedWaitUntil: | |
| 24 | visible: | |
| 25 | id: trail-full-map | |
| 26 | timeout: 15000 | |
| 27 | - takeScreenshot: android-native-dark-full-map | |
| 28 | - tapOn: | |
| 29 | id: map-profile-toggle | |
| 30 | - takeScreenshot: android-native-dark-collapsed-profile | |
| 31 | - back | |
| 32 | - scrollUntilVisible: | |
| 33 | element: | |
| 34 | id: trail-weather-heading | |
| 35 | direction: DOWN | |
| 36 | - takeScreenshot: android-native-dark-weather | |
| 37 | - scrollUntilVisible: | |
| 38 | element: | |
| 39 | id: trail-surfaces-heading | |
| 40 | direction: DOWN | |
| 41 | - takeScreenshot: android-native-dark-surfaces | |
| 42 | - assertVisible: | |
| 43 | id: trail-sticky-actions | |
| @@ -0,0 +1,16 @@ | ||
| 1 | appId: ${APP_ID} | |
| 2 | name: Open trailhead directions from the full native map | |
| 3 | --- | |
| 4 | - launchApp | |
| 5 | - openLink: wildloop://trail/${TRAIL_ID}/map | |
| 6 | - extendedWaitUntil: | |
| 7 | visible: | |
| 8 | id: map-directions | |
| 9 | timeout: 30000 | |
| 10 | - tapOn: | |
| 11 | id: map-directions | |
| 12 | - extendedWaitUntil: | |
| 13 | notVisible: | |
| 14 | id: trail-full-map | |
| 15 | timeout: 15000 | |
| 16 | - takeScreenshot: android-native-full-map-directions-handoff | |
| @@ -0,0 +1,16 @@ | ||
| 1 | appId: ${APP_ID} | |
| 2 | name: Open trailhead directions from the native sticky actions | |
| 3 | --- | |
| 4 | - launchApp | |
| 5 | - openLink: wildloop://trail/${TRAIL_ID} | |
| 6 | - extendedWaitUntil: | |
| 7 | visible: | |
| 8 | id: trail-directions | |
| 9 | timeout: 30000 | |
| 10 | - tapOn: | |
| 11 | id: trail-directions | |
| 12 | - extendedWaitUntil: | |
| 13 | notVisible: | |
| 14 | id: trail-sticky-actions | |
| 15 | timeout: 15000 | |
| 16 | - takeScreenshot: android-native-directions-handoff | |
| @@ -0,0 +1,192 @@ | ||
| 1 | appId: ${APP_ID} | |
| 2 | name: Native Android trail maps and sticky save | |
| 3 | --- | |
| 4 | - runFlow: sign-in.yaml | |
| 5 | - tapOn: | |
| 6 | id: trail-save | |
| 7 | - extendedWaitUntil: | |
| 8 | visible: '^Saved$' | |
| 9 | timeout: 15000 | |
| 10 | - openLink: wildloop://feed | |
| 11 | - extendedWaitUntil: | |
| 12 | visible: | |
| 13 | id: native-feed-scroll | |
| 14 | timeout: 30000 | |
| 15 | - openLink: wildloop://trail/${TRAIL_ID} | |
| 16 | - extendedWaitUntil: | |
| 17 | visible: '^Saved$' | |
| 18 | timeout: 30000 | |
| 19 | - tapOn: | |
| 20 | id: trail-save | |
| 21 | - extendedWaitUntil: | |
| 22 | visible: '^Save$' | |
| 23 | timeout: 15000 | |
| 24 | - openLink: wildloop://feed | |
| 25 | - extendedWaitUntil: | |
| 26 | visible: | |
| 27 | id: native-feed-scroll | |
| 28 | timeout: 30000 | |
| 29 | - openLink: wildloop://trail/${TRAIL_ID} | |
| 30 | - extendedWaitUntil: | |
| 31 | visible: '^Save$' | |
| 32 | timeout: 30000 | |
| 33 | - scrollUntilVisible: | |
| 34 | element: | |
| 35 | id: trail-elevation-slider | |
| 36 | direction: DOWN | |
| 37 | - tapOn: | |
| 38 | id: trail-elevation-slider | |
| 39 | point: '99.9%,50%' | |
| 40 | - extendedWaitUntil: | |
| 41 | visible: ${INLINE_PROFILE_END_TEXT} | |
| 42 | timeout: 15000 | |
| 43 | - takeScreenshot: android-native-inline-elevation-selection | |
| 44 | - tapOn: | |
| 45 | id: trail-profile-toggle | |
| 46 | - assertVisible: 'Toggle elevation profile, Elevation profile collapsed' | |
| 47 | - takeScreenshot: android-native-inline-profile-collapsed | |
| 48 | - tapOn: | |
| 49 | id: trail-profile-toggle | |
| 50 | - assertVisible: 'Toggle elevation profile, Elevation profile expanded' | |
| 51 | - extendedWaitUntil: | |
| 52 | visible: ${INLINE_PROFILE_END_TEXT} | |
| 53 | timeout: 15000 | |
| 54 | - takeScreenshot: android-native-inline-profile-restored | |
| 55 | - scrollUntilVisible: | |
| 56 | element: | |
| 57 | id: trail-zoom-in | |
| 58 | direction: DOWN | |
| 59 | - tapOn: | |
| 60 | id: trail-zoom-in | |
| 61 | - takeScreenshot: android-native-inline-zoom-in | |
| 62 | - tapOn: | |
| 63 | id: trail-zoom-out | |
| 64 | - takeScreenshot: android-native-inline-zoom-out | |
| 65 | - tapOn: | |
| 66 | id: trail-fit-route | |
| 67 | - takeScreenshot: android-native-inline-fit-route | |
| 68 | - tapOn: | |
| 69 | id: trail-map-3d | |
| 70 | - assertVisible: '^2D$' | |
| 71 | - takeScreenshot: android-native-inline-map-tilted | |
| 72 | - tapOn: '^North$' | |
| 73 | - takeScreenshot: android-native-inline-map-north | |
| 74 | - tapOn: | |
| 75 | id: trail-map-3d | |
| 76 | - assertVisible: '^3D$' | |
| 77 | - takeScreenshot: android-native-inline-map-flat | |
| 78 | - scrollUntilVisible: | |
| 79 | element: | |
| 80 | id: trail-map-layer | |
| 81 | direction: DOWN | |
| 82 | - tapOn: | |
| 83 | id: trail-map-layer | |
| 84 | - tapOn: '^Satellite$' | |
| 85 | - assertVisible: 'Map layer, Satellite' | |
| 86 | - takeScreenshot: android-native-inline-map-satellite | |
| 87 | - tapOn: | |
| 88 | id: trail-map-layer | |
| 89 | - tapOn: '^Hybrid$' | |
| 90 | - assertVisible: 'Map layer, Hybrid' | |
| 91 | - takeScreenshot: android-native-inline-map-hybrid | |
| 92 | - tapOn: | |
| 93 | id: trail-map-layer | |
| 94 | - tapOn: '^Street$' | |
| 95 | - assertVisible: 'Map layer, Street' | |
| 96 | - takeScreenshot: android-native-inline-map-street | |
| 97 | - tapOn: | |
| 98 | id: trail-map-layer | |
| 99 | - tapOn: '^Outdoors$' | |
| 100 | - assertVisible: 'Map layer, Outdoors' | |
| 101 | - takeScreenshot: android-native-inline-map-outdoors | |
| 102 | - scrollUntilVisible: | |
| 103 | element: | |
| 104 | id: trail-map-fullscreen | |
| 105 | direction: UP | |
| 106 | - assertVisible: | |
| 107 | id: trail-sticky-actions | |
| 108 | - tapOn: | |
| 109 | id: trail-map-fullscreen | |
| 110 | - extendedWaitUntil: | |
| 111 | visible: | |
| 112 | id: trail-full-map | |
| 113 | timeout: 30000 | |
| 114 | - takeScreenshot: android-native-map-outdoors | |
| 115 | - tapOn: | |
| 116 | id: map-zoom-in | |
| 117 | - takeScreenshot: android-native-fullscreen-zoom-in | |
| 118 | - tapOn: | |
| 119 | id: map-zoom-out | |
| 120 | - takeScreenshot: android-native-fullscreen-zoom-out | |
| 121 | - tapOn: | |
| 122 | id: map-fit | |
| 123 | - takeScreenshot: android-native-fullscreen-fit-route | |
| 124 | - tapOn: | |
| 125 | id: map-3d | |
| 126 | - assertVisible: '^2D$' | |
| 127 | - takeScreenshot: android-native-fullscreen-map-tilted | |
| 128 | - tapOn: | |
| 129 | id: map-north | |
| 130 | - takeScreenshot: android-native-fullscreen-map-north | |
| 131 | - tapOn: | |
| 132 | id: map-3d | |
| 133 | - assertVisible: '^3D$' | |
| 134 | - takeScreenshot: android-native-fullscreen-map-flat | |
| 135 | - tapOn: | |
| 136 | id: map-profile-toggle | |
| 137 | - assertVisible: 'Toggle elevation profile, Elevation profile collapsed' | |
| 138 | - takeScreenshot: android-native-map-profile-collapsed | |
| 139 | - tapOn: | |
| 140 | id: map-profile-toggle | |
| 141 | - assertVisible: 'Toggle elevation profile, Elevation profile expanded' | |
| 142 | - tapOn: | |
| 143 | id: map-elevation-slider | |
| 144 | point: '99.9%,50%' | |
| 145 | - extendedWaitUntil: | |
| 146 | visible: ${PROFILE_END_TEXT} | |
| 147 | timeout: 15000 | |
| 148 | - takeScreenshot: android-native-fullscreen-elevation-selection | |
| 149 | - tapOn: | |
| 150 | id: map-fit | |
| 151 | - tapOn: | |
| 152 | id: map-layer | |
| 153 | - tapOn: '^Satellite$' | |
| 154 | - assertVisible: 'Map layer, Satellite' | |
| 155 | - takeScreenshot: android-native-map-satellite | |
| 156 | - tapOn: | |
| 157 | id: map-layer | |
| 158 | - tapOn: '^Hybrid$' | |
| 159 | - assertVisible: 'Map layer, Hybrid' | |
| 160 | - takeScreenshot: android-native-map-hybrid | |
| 161 | - tapOn: | |
| 162 | id: map-layer | |
| 163 | - tapOn: '^Street$' | |
| 164 | - assertVisible: 'Map layer, Street' | |
| 165 | - takeScreenshot: android-native-map-street | |
| 166 | - tapOn: | |
| 167 | id: map-layer | |
| 168 | - tapOn: '^Outdoors$' | |
| 169 | - assertVisible: 'Map layer, Outdoors' | |
| 170 | - tapOn: | |
| 171 | id: map-fit | |
| 172 | - takeScreenshot: android-native-map-outdoors-restored | |
| 173 | - back | |
| 174 | - assertVisible: | |
| 175 | id: trail-sticky-actions | |
| 176 | - takeScreenshot: android-native-sticky-actions | |
| 177 | - scrollUntilVisible: | |
| 178 | element: | |
| 179 | id: trail-weather-heading | |
| 180 | direction: DOWN | |
| 181 | - assertVisible: | |
| 182 | id: trail-weather-heading | |
| 183 | - takeScreenshot: android-native-trail-weather | |
| 184 | - scrollUntilVisible: | |
| 185 | element: | |
| 186 | id: trail-surfaces-heading | |
| 187 | direction: DOWN | |
| 188 | - assertVisible: | |
| 189 | id: trail-surfaces-heading | |
| 190 | - takeScreenshot: android-native-trail-surfaces | |
| 191 | - assertVisible: | |
| 192 | id: trail-sticky-actions | |
| @@ -0,0 +1,37 @@ | ||
| 1 | appId: ${APP_ID} | |
| 2 | name: Create a review with native controls | |
| 3 | --- | |
| 4 | - runFlow: sign-in.yaml | |
| 5 | - scrollUntilVisible: | |
| 6 | element: | |
| 7 | id: trail-write-review | |
| 8 | direction: DOWN | |
| 9 | - assertVisible: | |
| 10 | id: trail-sticky-actions | |
| 11 | - tapOn: | |
| 12 | id: trail-write-review | |
| 13 | - extendedWaitUntil: | |
| 14 | visible: | |
| 15 | id: trail-review-content | |
| 16 | timeout: 15000 | |
| 17 | - tapOn: | |
| 18 | id: trail-review-star-5 | |
| 19 | - tapOn: | |
| 20 | id: trail-review-content | |
| 21 | - inputText: ${REVIEW_TEXT} | |
| 22 | - hideKeyboard | |
| 23 | - scrollUntilVisible: | |
| 24 | element: | |
| 25 | id: trail-review-submit | |
| 26 | direction: DOWN | |
| 27 | - tapOn: | |
| 28 | id: trail-review-submit | |
| 29 | - extendedWaitUntil: | |
| 30 | visible: 'Edit your review' | |
| 31 | timeout: 15000 | |
| 32 | - scrollUntilVisible: | |
| 33 | element: | |
| 34 | text: ${REVIEW_TEXT} | |
| 35 | direction: DOWN | |
| 36 | - assertVisible: ${REVIEW_TEXT} | |
| 37 | - takeScreenshot: android-native-review-created | |
| @@ -0,0 +1,25 @@ | ||
| 1 | appId: ${APP_ID} | |
| 2 | name: Delete the persisted review through native confirmation | |
| 3 | --- | |
| 4 | - launchApp | |
| 5 | - openLink: wildloop://trail/${TRAIL_ID} | |
| 6 | - extendedWaitUntil: | |
| 7 | visible: ${TRAIL_NAME} | |
| 8 | timeout: 30000 | |
| 9 | - scrollUntilVisible: | |
| 10 | element: | |
| 11 | id: trail-delete-review | |
| 12 | direction: DOWN | |
| 13 | - tapOn: | |
| 14 | id: trail-delete-review | |
| 15 | - tapOn: '^Delete review$' | |
| 16 | - extendedWaitUntil: | |
| 17 | visible: 'Leave a review' | |
| 18 | timeout: 15000 | |
| 19 | - assertNotVisible: ${REVIEW_TEXT} | |
| 20 | - assertNotVisible: ${EDITED_REVIEW_TEXT} | |
| 21 | - assertNotVisible: | |
| 22 | id: trail-delete-review | |
| 23 | - assertVisible: | |
| 24 | id: trail-sticky-actions | |
| 25 | - takeScreenshot: android-native-review-deleted | |
| @@ -0,0 +1,48 @@ | ||
| 1 | appId: ${APP_ID} | |
| 2 | name: Reopen and edit the persisted native review | |
| 3 | --- | |
| 4 | - launchApp | |
| 5 | - openLink: wildloop://trail/${TRAIL_ID} | |
| 6 | - extendedWaitUntil: | |
| 7 | visible: ${TRAIL_NAME} | |
| 8 | timeout: 30000 | |
| 9 | - scrollUntilVisible: | |
| 10 | element: | |
| 11 | id: trail-write-review | |
| 12 | direction: DOWN | |
| 13 | - tapOn: | |
| 14 | id: trail-write-review | |
| 15 | - extendedWaitUntil: | |
| 16 | visible: | |
| 17 | id: trail-review-content | |
| 18 | timeout: 15000 | |
| 19 | - tapOn: | |
| 20 | id: trail-review-content | |
| 21 | - longPressOn: | |
| 22 | id: trail-review-content | |
| 23 | - takeScreenshot: android-native-review-edit-menu | |
| 24 | - tapOn: '(?i)^Select all$' | |
| 25 | - eraseText: 1 | |
| 26 | - inputText: ${EDITED_REVIEW_TEXT} | |
| 27 | - assertVisible: | |
| 28 | id: trail-review-content | |
| 29 | text: ${EDITED_REVIEW_TEXT} | |
| 30 | - hideKeyboard | |
| 31 | - tapOn: | |
| 32 | id: trail-review-star-4 | |
| 33 | - scrollUntilVisible: | |
| 34 | element: | |
| 35 | id: trail-review-submit | |
| 36 | direction: DOWN | |
| 37 | - tapOn: | |
| 38 | id: trail-review-submit | |
| 39 | - extendedWaitUntil: | |
| 40 | visible: 'Edit your review' | |
| 41 | timeout: 15000 | |
| 42 | - scrollUntilVisible: | |
| 43 | element: | |
| 44 | text: ${EDITED_REVIEW_TEXT} | |
| 45 | direction: DOWN | |
| 46 | - assertVisible: ${EDITED_REVIEW_TEXT} | |
| 47 | - assertNotVisible: ${REVIEW_TEXT} | |
| 48 | - takeScreenshot: android-native-review-edited | |
| @@ -0,0 +1,30 @@ | ||
| 1 | appId: ${APP_ID} | |
| 2 | name: Copy the trail link and paste it into a real native field | |
| 3 | --- | |
| 4 | - launchApp | |
| 5 | - openLink: wildloop://trail/${TRAIL_ID} | |
| 6 | - extendedWaitUntil: | |
| 7 | visible: | |
| 8 | id: trail-share | |
| 9 | timeout: 30000 | |
| 10 | - tapOn: | |
| 11 | id: trail-share | |
| 12 | - extendedWaitUntil: | |
| 13 | visible: '^Copy$' | |
| 14 | timeout: 15000 | |
| 15 | - takeScreenshot: android-native-share-sheet | |
| 16 | - tapOn: '^Copy$' | |
| 17 | - openLink: wildloop://search | |
| 18 | - extendedWaitUntil: | |
| 19 | visible: | |
| 20 | id: search-input | |
| 21 | timeout: 15000 | |
| 22 | - tapOn: | |
| 23 | id: search-input | |
| 24 | - longPressOn: | |
| 25 | id: search-input | |
| 26 | - tapOn: '^Paste$' | |
| 27 | - assertVisible: | |
| 28 | id: search-input | |
| 29 | text: 'https://wildloop.org/trail/${TRAIL_ID}' | |
| 30 | - takeScreenshot: android-native-share-link-pasted | |
| @@ -0,0 +1,27 @@ | ||
| 1 | appId: ${APP_ID} | |
| 2 | --- | |
| 3 | - launchApp: | |
| 4 | clearState: true | |
| 5 | - extendedWaitUntil: | |
| 6 | visible: | |
| 7 | id: native-feed-scroll | |
| 8 | timeout: 30000 | |
| 9 | - openLink: wildloop://login?next=/trail/${TRAIL_ID} | |
| 10 | - extendedWaitUntil: | |
| 11 | visible: | |
| 12 | id: login-email | |
| 13 | timeout: 30000 | |
| 14 | - tapOn: | |
| 15 | id: login-email | |
| 16 | - inputText: ${E2E_EMAIL} | |
| 17 | - tapOn: | |
| 18 | id: login-password | |
| 19 | - inputText: ${E2E_PASSWORD} | |
| 20 | - hideKeyboard | |
| 21 | - tapOn: | |
| 22 | id: login-submit | |
| 23 | - extendedWaitUntil: | |
| 24 | visible: ${TRAIL_NAME} | |
| 25 | timeout: 30000 | |
| 26 | - assertVisible: | |
| 27 | id: trail-sticky-actions | |
| @@ -0,0 +1,99 @@ | ||
| 1 | appId: ${APP_ID} | |
| 2 | name: Native location, route customization and GPX sharing | |
| 3 | --- | |
| 4 | - launchApp | |
| 5 | - openLink: wildloop://trail/${TRAIL_ID}/map | |
| 6 | - extendedWaitUntil: | |
| 7 | visible: | |
| 8 | id: trail-full-map | |
| 9 | timeout: 30000 | |
| 10 | - assertNotVisible: | |
| 11 | id: '.*permission_allow_foreground_only_button' | |
| 12 | - tapOn: | |
| 13 | id: map-zoom-in | |
| 14 | - tapOn: | |
| 15 | id: map-3d | |
| 16 | - assertVisible: | |
| 17 | id: map-3d | |
| 18 | text: '^2D$' | |
| 19 | - takeScreenshot: android-native-camera-before-location | |
| 20 | - tapOn: | |
| 21 | id: map-locate | |
| 22 | - extendedWaitUntil: | |
| 23 | visible: | |
| 24 | id: '.*permission_allow_foreground_only_button' | |
| 25 | timeout: 15000 | |
| 26 | - tapOn: | |
| 27 | id: '.*permission_allow_foreground_only_button' | |
| 28 | - extendedWaitUntil: | |
| 29 | visible: 'My location, Your location is shown' | |
| 30 | timeout: 30000 | |
| 31 | - assertVisible: | |
| 32 | id: map-3d | |
| 33 | text: '^2D$' | |
| 34 | - takeScreenshot: android-native-map-location | |
| 35 | - tapOn: | |
| 36 | id: map-customize | |
| 37 | - extendedWaitUntil: | |
| 38 | visible: | |
| 39 | id: route-builder-map | |
| 40 | timeout: 30000 | |
| 41 | - assertVisible: ${TRAIL_NAME} | |
| 42 | - takeScreenshot: android-native-customize-route | |
| 43 | - back | |
| 44 | - assertVisible: | |
| 45 | id: trail-full-map | |
| 46 | - tapOn: | |
| 47 | id: map-export | |
| 48 | - extendedWaitUntil: | |
| 49 | visible: ${GPX_NAME} | |
| 50 | timeout: 15000 | |
| 51 | - takeScreenshot: android-native-gpx-share-sheet | |
| 52 | - back | |
| 53 | - assertVisible: | |
| 54 | id: trail-full-map | |
| 55 | - openLink: wildloop://trail/${TRAIL_ID} | |
| 56 | - extendedWaitUntil: | |
| 57 | visible: | |
| 58 | id: trail-sticky-actions | |
| 59 | timeout: 30000 | |
| 60 | - scrollUntilVisible: | |
| 61 | element: | |
| 62 | id: trail-zoom-in | |
| 63 | direction: DOWN | |
| 64 | - tapOn: | |
| 65 | id: trail-zoom-in | |
| 66 | - scrollUntilVisible: | |
| 67 | element: | |
| 68 | id: trail-map-3d | |
| 69 | direction: DOWN | |
| 70 | - tapOn: | |
| 71 | id: trail-map-3d | |
| 72 | - assertVisible: | |
| 73 | id: trail-map-3d | |
| 74 | text: '^2D$' | |
| 75 | - takeScreenshot: android-native-inline-camera-before-location | |
| 76 | - tapOn: | |
| 77 | id: trail-map-locate | |
| 78 | - extendedWaitUntil: | |
| 79 | visible: 'My location, Your location is shown' | |
| 80 | timeout: 30000 | |
| 81 | - assertNotVisible: | |
| 82 | id: '.*permission_allow_foreground_only_button' | |
| 83 | - assertVisible: | |
| 84 | id: trail-map-3d | |
| 85 | text: '^2D$' | |
| 86 | - takeScreenshot: android-native-inline-map-location | |
| 87 | - scrollUntilVisible: | |
| 88 | element: | |
| 89 | id: trail-map-export | |
| 90 | direction: DOWN | |
| 91 | - tapOn: | |
| 92 | id: trail-map-export | |
| 93 | - extendedWaitUntil: | |
| 94 | visible: ${GPX_NAME} | |
| 95 | timeout: 15000 | |
| 96 | - takeScreenshot: android-native-inline-gpx-share-sheet | |
| 97 | - back | |
| 98 | - assertVisible: | |
| 99 | id: trail-sticky-actions | |
bun.lockmodified+149-115
| @@ -8,12 +8,12 @@ | ||
| 8 | 8 | "@bughq/stacks": "^0.2.2", |
| 9 | 9 | "@iconify-json/lucide": "^1.2.137", |
| 10 | 10 | "@loghq/stacks": "^0.2.6", |
| 11 | "@stacksjs/composables": "^0.75.219", | |
| 12 | "@stacksjs/mobile": "^0.75.219", | |
| 13 | "@stacksjs/stx": "^0.2.563", | |
| 11 | "@stacksjs/composables": "^0.75.221", | |
| 12 | "@stacksjs/mobile": "^0.75.221", | |
| 13 | "@stacksjs/stx": "^0.2.573", | |
| 14 | 14 | "@ts-analytics/tracking": "^0.1.17", |
| 15 | "craft-native": "^0.0.237", | |
| 16 | "stacks": "^0.75.219", | |
| 15 | "craft-native": "^0.0.244", | |
| 16 | "stacks": "^0.75.221", | |
| 17 | 17 | "ts-health": "^0.1.0", |
| 18 | 18 | "ts-maps": "^0.5.5", |
| 19 | 19 | "ts-qr-codes": "^0.1.8", |
| @@ -41,31 +41,31 @@ | ||
| 41 | 41 | |
| 42 | 42 | "@loghq/stacks": ["@loghq/stacks@0.2.6", "", {}, "sha512-7E4v4nWUDgKuuyFZwa5VZxuRGewMGwAcclCAcm+fm36rGdWnLx6TTdRwMhADlzAYw3nCRyljpnVrViiExQ0UDg=="], |
| 43 | 43 | |
| 44 | "@stacksjs/action-runner": ["@stacksjs/action-runner@0.75.219", "", { "dependencies": { "@stacksjs/enums": "0.75.219", "@stacksjs/types": "0.75.219" } }, "sha512-pnFyK+Hl1FvqSYsNX+i78v6fJqdSm93qctaeNIlxlcCozMc/r9lfyOidIwRp4UlCYU5imU6GAF+oio6g0IZQVQ=="], | |
| 44 | "@stacksjs/action-runner": ["@stacksjs/action-runner@0.75.221", "", { "dependencies": { "@stacksjs/enums": "0.75.221", "@stacksjs/types": "0.75.221" } }, "sha512-BAVajHkw5iTFTt2yGU5Zb+G3HtsdQOk7ksKQmqPqFbUo4HDxtsNTrj5kJlBO98FAGkiBhTMvaSBxoDNGkZjurw=="], | |
| 45 | 45 | |
| 46 | "@stacksjs/actions": ["@stacksjs/actions@0.75.219", "", { "dependencies": { "@stacksjs/action-runner": "0.75.219", "@stacksjs/ai": "0.75.219", "@stacksjs/analytics": "0.75.219", "@stacksjs/api": "0.75.219", "@stacksjs/auth": "0.75.219", "@stacksjs/build": "0.75.219", "@stacksjs/bumpx": "^0.2.21", "@stacksjs/bunpress": "^0.2.15", "@stacksjs/cli": "0.75.219", "@stacksjs/cloud": "0.75.219", "@stacksjs/cms": "0.75.219", "@stacksjs/components": "^0.2.534", "@stacksjs/config": "0.75.219", "@stacksjs/database": "0.75.219", "@stacksjs/desktop-build": "0.75.219", "@stacksjs/dns": "0.75.219", "@stacksjs/enums": "0.75.219", "@stacksjs/env": "0.75.219", "@stacksjs/error-handling": "0.75.219", "@stacksjs/github": "0.75.219", "@stacksjs/gitit": "^0.2.5", "@stacksjs/i18n": "0.75.219", "@stacksjs/image": "0.75.219", "@stacksjs/logging": "0.75.219", "@stacksjs/logsmith": "^0.2.11", "@stacksjs/mobile": "0.75.219", "@stacksjs/orm": "0.75.219", "@stacksjs/path": "0.75.219", "@stacksjs/payments": "0.75.219", "@stacksjs/queue": "0.75.219", "@stacksjs/registry": "0.75.219", "@stacksjs/router": "0.75.219", "@stacksjs/rpx": "^0.11.54", "@stacksjs/scheduler": "0.75.219", "@stacksjs/search-engine": "0.75.219", "@stacksjs/security": "0.75.219", "@stacksjs/server": "0.75.219", "@stacksjs/sites": "0.75.219", "@stacksjs/storage": "0.75.219", "@stacksjs/strings": "0.75.219", "@stacksjs/stx": "^0.2.534", "@stacksjs/tlsx": "^0.13.19", "@stacksjs/ts-cloud": "^0.16.58", "@stacksjs/ts-md": "^0.1.1", "@stacksjs/tunnel": "0.75.219", "@stacksjs/types": "0.75.219", "@stacksjs/utils": "0.75.219", "@stacksjs/whois": "0.75.219", "bun-plugin-stx": "^0.2.534" }, "peerDependencies": { "craft-native": ">=0.0.163", "pickier": "^0.1.70" }, "optionalPeers": ["craft-native", "pickier"] }, "sha512-anDtPH4ODwHYl1iDZLhD2C9GD4JRNnKAvtTs2UuBOt6f7sn++VhxWE49Fl4vjWIbsyjI47cwXUcF/MYNqVI5rw=="], | |
| 46 | "@stacksjs/actions": ["@stacksjs/actions@0.75.221", "", { "dependencies": { "@stacksjs/action-runner": "0.75.221", "@stacksjs/ai": "0.75.221", "@stacksjs/analytics": "0.75.221", "@stacksjs/api": "0.75.221", "@stacksjs/auth": "0.75.221", "@stacksjs/build": "0.75.221", "@stacksjs/bumpx": "^0.2.21", "@stacksjs/bunpress": "^0.2.15", "@stacksjs/cli": "0.75.221", "@stacksjs/cloud": "0.75.221", "@stacksjs/cms": "0.75.221", "@stacksjs/components": "^0.2.534", "@stacksjs/config": "0.75.221", "@stacksjs/database": "0.75.221", "@stacksjs/desktop-build": "0.75.221", "@stacksjs/dns": "0.75.221", "@stacksjs/enums": "0.75.221", "@stacksjs/env": "0.75.221", "@stacksjs/error-handling": "0.75.221", "@stacksjs/github": "0.75.221", "@stacksjs/gitit": "^0.2.5", "@stacksjs/i18n": "0.75.221", "@stacksjs/image": "0.75.221", "@stacksjs/logging": "0.75.221", "@stacksjs/logsmith": "^0.2.11", "@stacksjs/mobile": "0.75.221", "@stacksjs/orm": "0.75.221", "@stacksjs/path": "0.75.221", "@stacksjs/payments": "0.75.221", "@stacksjs/queue": "0.75.221", "@stacksjs/registry": "0.75.221", "@stacksjs/router": "0.75.221", "@stacksjs/rpx": "^0.11.54", "@stacksjs/scheduler": "0.75.221", "@stacksjs/search-engine": "0.75.221", "@stacksjs/security": "0.75.221", "@stacksjs/server": "0.75.221", "@stacksjs/sites": "0.75.221", "@stacksjs/storage": "0.75.221", "@stacksjs/strings": "0.75.221", "@stacksjs/stx": "^0.2.534", "@stacksjs/tlsx": "^0.13.19", "@stacksjs/ts-cloud": "^0.16.58", "@stacksjs/ts-md": "^0.1.1", "@stacksjs/tunnel": "0.75.221", "@stacksjs/types": "0.75.221", "@stacksjs/utils": "0.75.221", "@stacksjs/whois": "0.75.221", "bun-plugin-stx": "^0.2.534" }, "peerDependencies": { "craft-native": ">=0.0.163", "pickier": "^0.1.70" }, "optionalPeers": ["craft-native", "pickier"] }, "sha512-ySyi/LzeN7ySByOpUQ6DMSi1Orm0Bjxh+BQ3TrK1rsoSgfuBRam+/DzPp++xJOtWACyCsU8AEHptPG3ReYR7kg=="], | |
| 47 | 47 | |
| 48 | "@stacksjs/ai": ["@stacksjs/ai@0.75.219", "", { "dependencies": { "@stacksjs/cli": "0.75.219", "@stacksjs/config": "0.75.219", "@stacksjs/router": "0.75.219", "@stacksjs/storage": "0.75.219", "@stacksjs/ts-cloud": "^0.16.58" }, "peerDependencies": { "@anthropic-ai/claude-agent-sdk": "^0.2.79" }, "optionalPeers": ["@anthropic-ai/claude-agent-sdk"] }, "sha512-A252eE6w4eA/toEOpprcrEUkTtSx3NfSaaBScybS/eX0qAaNModXxb+jnFoEqzbtW7iZxVMaw/gfWUiSYq+kRA=="], | |
| 48 | "@stacksjs/ai": ["@stacksjs/ai@0.75.221", "", { "dependencies": { "@stacksjs/cli": "0.75.221", "@stacksjs/config": "0.75.221", "@stacksjs/router": "0.75.221", "@stacksjs/storage": "0.75.221", "@stacksjs/ts-cloud": "^0.16.58" }, "peerDependencies": { "@anthropic-ai/claude-agent-sdk": "^0.2.79" }, "optionalPeers": ["@anthropic-ai/claude-agent-sdk"] }, "sha512-TzwT/7cWVHh5P1vJTE5z2tf1Ad9AGwlApokblpB8EKpQlRFCTX2onnCrVJzwM/wHVCLh/46viTnebesfWMP8YA=="], | |
| 49 | 49 | |
| 50 | "@stacksjs/alias": ["@stacksjs/alias@0.75.219", "", { "dependencies": { "@stacksjs/path": "0.75.219" } }, "sha512-nufSHMvF3Mhako3m31nierIQnj2KPE4GK2xOPmUi55O9soWEiHhFw/2xSvyb0aDy73OUE2/cX1eEJ5gwN9TgmQ=="], | |
| 50 | "@stacksjs/alias": ["@stacksjs/alias@0.75.221", "", { "dependencies": { "@stacksjs/path": "0.75.221" } }, "sha512-LbTx3LBkSr9zGC7LReyhZrx3nHezcgHcdj1+Q2qIu59yYNev/frRnj568e+FzIf138pM2MjCvLAro+pWZBKVPg=="], | |
| 51 | 51 | |
| 52 | "@stacksjs/analytics": ["@stacksjs/analytics@0.75.219", "", { "dependencies": { "@stacksjs/database": "0.75.219", "@stacksjs/env": "0.75.219", "@stacksjs/types": "0.75.219" } }, "sha512-L5wgVHuROHQnATVVbBqPQwYT5wJoXTXDgL1NdaZ1KPCrclK1T+WY+2fBElEl8SJA2rKEnOgsC2Xgzw+l+BW/dw=="], | |
| 52 | "@stacksjs/analytics": ["@stacksjs/analytics@0.75.221", "", { "dependencies": { "@stacksjs/database": "0.75.221", "@stacksjs/env": "0.75.221", "@stacksjs/types": "0.75.221" } }, "sha512-zkTqsIUqTq8YSjMO44h7sN3YZZiuAdKVe3tGbEQQ+kagzp9OXBwBLfzw8al/hIuKXtugDBitc3eywCzPH9PF0g=="], | |
| 53 | 53 | |
| 54 | "@stacksjs/api": ["@stacksjs/api@0.75.219", "", { "dependencies": { "@stacksjs/config": "0.75.219", "@stacksjs/path": "0.75.219" }, "peerDependencies": { "@stacksjs/router": "0.75.219" }, "optionalPeers": ["@stacksjs/router"] }, "sha512-XHLgopMyvCxKDRMLjdFo53f9iE+fEIWdjXzPV0gLFTEOJHF7aBEKpB8kM8535Ohq9heYIT/X0qaa/AwEoOPVcA=="], | |
| 54 | "@stacksjs/api": ["@stacksjs/api@0.75.221", "", { "dependencies": { "@stacksjs/config": "0.75.221", "@stacksjs/path": "0.75.221" }, "peerDependencies": { "@stacksjs/router": "0.75.221" }, "optionalPeers": ["@stacksjs/router"] }, "sha512-CIBkH9BfQKgaKs+fOY0bfkJvAY+nxZkuWuSEdKK+OyiA/Zx5X9w/Lh/t/hYG9hkLIj6hOQF+wHKTqK4mGBUH9w=="], | |
| 55 | 55 | |
| 56 | "@stacksjs/arrays": ["@stacksjs/arrays@0.75.219", "", {}, "sha512-oAppaMlKWOzl/++iCcMrV9L1ReU5sIKsa3be2iPtHAmNTkDDmvU8SndefbW5PKecExWnb9zwMwxK+E1vMCT6Pw=="], | |
| 56 | "@stacksjs/arrays": ["@stacksjs/arrays@0.75.221", "", {}, "sha512-+q1xYA7xllZgsenahvVerp9zDWJHDc76TtwhfaynvMN50eZvS639iT2uz5vaQIQe2eSYwrGHb50tlF6zmKztgg=="], | |
| 57 | 57 | |
| 58 | "@stacksjs/audio": ["@stacksjs/audio@0.75.219", "", { "dependencies": { "@ts-audio/core": "^0.1.2", "ts-video-player": "^0.1.6" } }, "sha512-tlrtVQwOjZbYadqw9WggKuFvY062Dp7qgkjbcDhj/CcLFjlF8Mv0aXXgYWNefIr4q7FLH0RyQ6tf+VIrm/wcRg=="], | |
| 58 | "@stacksjs/audio": ["@stacksjs/audio@0.75.221", "", { "dependencies": { "@ts-audio/core": "^0.1.2", "ts-video-player": "^0.1.6" } }, "sha512-bVtpJ1aeaBp6hi3qBcT9Vt+G6LAha/Ws9NXzwmevkWpsmi5ajN58ZbOx3vNqMLjqlNficPZOHcAppGg00l4ofQ=="], | |
| 59 | 59 | |
| 60 | "@stacksjs/auth": ["@stacksjs/auth@0.75.219", "", { "dependencies": { "@stacksjs/bun-router": "^0.1.24", "@stacksjs/cache": "0.75.219", "@stacksjs/config": "0.75.219", "@stacksjs/database": "0.75.219", "@stacksjs/email": "0.75.219", "@stacksjs/env": "0.75.219", "@stacksjs/error-handling": "0.75.219", "@stacksjs/logging": "0.75.219", "@stacksjs/orm": "0.75.219", "@stacksjs/path": "0.75.219", "@stacksjs/router": "0.75.219", "@stacksjs/security": "0.75.219", "@stacksjs/storage": "0.75.219", "@stacksjs/ts-auth": "^0.4.7", "ts-qr-codes": "^0.1.8" } }, "sha512-/bpA8jrbItusUdqzTbKUZe6/E6VDiEy9YcWBnkZHz3Xc0wjlaTs30W0TOxUGvSoFUpmLhfyCpe+TiE3VpwVecg=="], | |
| 60 | "@stacksjs/auth": ["@stacksjs/auth@0.75.221", "", { "dependencies": { "@stacksjs/bun-router": "^0.1.24", "@stacksjs/cache": "0.75.221", "@stacksjs/config": "0.75.221", "@stacksjs/database": "0.75.221", "@stacksjs/email": "0.75.221", "@stacksjs/env": "0.75.221", "@stacksjs/error-handling": "0.75.221", "@stacksjs/logging": "0.75.221", "@stacksjs/orm": "0.75.221", "@stacksjs/path": "0.75.221", "@stacksjs/router": "0.75.221", "@stacksjs/security": "0.75.221", "@stacksjs/storage": "0.75.221", "@stacksjs/ts-auth": "^0.4.7", "ts-qr-codes": "^0.1.8" } }, "sha512-yXzr2XeTUqQbjAitj1ZPrO6R7X0yHqVbWUibfsjlqrMTictbDNWJZG/ZqGFe5ea2iKBwwa9LoU9yi+lN6LwCjw=="], | |
| 61 | 61 | |
| 62 | "@stacksjs/browser": ["@stacksjs/browser@0.75.219", "", { "dependencies": { "@stacksjs/composables": "0.75.219", "@stacksjs/datetime": "0.75.219", "@stacksjs/strings": "0.75.219", "@stacksjs/stx": "^0.2.534", "bun-query-builder": "^0.3.8" }, "peerDependencies": { "@stripe/stripe-js": "^10.0.0" }, "optionalPeers": ["@stripe/stripe-js"] }, "sha512-+JS80mVOhUNJRdQkbCEVYvU7kEso0LvQ8/FnET/TNDmdqdyIW6k47QBNJAiCOxblkbcpuFJKUb0mkSMaM1iDYA=="], | |
| 62 | "@stacksjs/browser": ["@stacksjs/browser@0.75.221", "", { "dependencies": { "@stacksjs/composables": "0.75.221", "@stacksjs/datetime": "0.75.221", "@stacksjs/strings": "0.75.221", "@stacksjs/stx": "^0.2.534", "bun-query-builder": "^0.3.8" }, "peerDependencies": { "@stripe/stripe-js": "^10.0.0" }, "optionalPeers": ["@stripe/stripe-js"] }, "sha512-MIW+qyTzUq6CqodgdLzrWUNUIOvoNmj5zek7ZpzfwQoc582VezLN+3eRegF0tIsHF8XUzbway1WUhKAMbG/8Mw=="], | |
| 63 | 63 | |
| 64 | "@stacksjs/browser-extension": ["@stacksjs/browser-extension@0.75.219", "", { "dependencies": { "bun-plugin-stx": "^0.2.534", "ts-pantry": "^0.11.69" }, "peerDependencies": { "web-ext": ">=10.7.0" }, "optionalPeers": ["web-ext"] }, "sha512-ZEQlAKUx8VEefU7eUrYiTpon95O20t3/bCWOjZ4Iiq73jv6O2muK8iki4E1bCLF3iplLfYo323oHzHcJDpt6gw=="], | |
| 64 | "@stacksjs/browser-extension": ["@stacksjs/browser-extension@0.75.221", "", { "dependencies": { "bun-plugin-stx": "^0.2.534", "ts-pantry": "^0.11.69" }, "peerDependencies": { "web-ext": ">=10.7.0" }, "optionalPeers": ["web-ext"] }, "sha512-2vrM6o4hisE45JY/9qldvSMnsppEbdo8yaMpmbHKkzR2DhYWESiyYtlUnarCK3ClaHNCS9boTcWRad2hHtPcpQ=="], | |
| 65 | 65 | |
| 66 | "@stacksjs/buddy": ["@stacksjs/buddy@0.75.219", "", { "dependencies": { "@stacksjs/actions": "0.75.219", "@stacksjs/ai": "0.75.219", "@stacksjs/alias": "0.75.219", "@stacksjs/analytics": "0.75.219", "@stacksjs/api": "0.75.219", "@stacksjs/arrays": "0.75.219", "@stacksjs/auth": "0.75.219", "@stacksjs/browser-extension": "0.75.219", "@stacksjs/build": "0.75.219", "@stacksjs/bumpx": "^0.2.21", "@stacksjs/cache": "0.75.219", "@stacksjs/chat": "0.75.219", "@stacksjs/clapp": "^0.2.16", "@stacksjs/cli": "0.75.219", "@stacksjs/cloud": "0.75.219", "@stacksjs/cms": "0.75.219", "@stacksjs/collections": "0.75.219", "@stacksjs/commerce": "0.75.219", "@stacksjs/config": "0.75.219", "@stacksjs/database": "0.75.219", "@stacksjs/desktop-build": "0.75.219", "@stacksjs/dns": "0.75.219", "@stacksjs/dnsx": "^0.2.3", "@stacksjs/email": "0.75.219", "@stacksjs/enums": "0.75.219", "@stacksjs/env": "0.75.219", "@stacksjs/error-handling": "0.75.219", "@stacksjs/events": "0.75.219", "@stacksjs/features": "0.75.219", "@stacksjs/git": "0.75.219", "@stacksjs/gitit": "^0.2.5", "@stacksjs/health": "0.75.219", "@stacksjs/httx": "^0.1.10", "@stacksjs/image": "0.75.219", "@stacksjs/lint": "0.75.219", "@stacksjs/logging": "0.75.219", "@stacksjs/notifications": "0.75.219", "@stacksjs/objects": "0.75.219", "@stacksjs/orm": "0.75.219", "@stacksjs/path": "0.75.219", "@stacksjs/payments": "0.75.219", "@stacksjs/realtime": "0.75.219", "@stacksjs/router": "0.75.219", "@stacksjs/rpx": "^0.11.54", "@stacksjs/scheduler": "0.75.219", "@stacksjs/search-engine": "0.75.219", "@stacksjs/security": "0.75.219", "@stacksjs/server": "0.75.219", "@stacksjs/sites": "0.75.219", "@stacksjs/skills": "0.75.219", "@stacksjs/socials": "0.75.219", "@stacksjs/storage": "0.75.219", "@stacksjs/strings": "0.75.219", "@stacksjs/stx": "^0.2.534", "@stacksjs/testing": "0.75.219", "@stacksjs/tinker": "0.75.219", "@stacksjs/tlsx": "^0.13.19", "@stacksjs/ts-cloud": "^0.16.58", "@stacksjs/tunnel": "0.75.219", "@stacksjs/types": "0.75.219", "@stacksjs/ui": "0.75.219", "@stacksjs/utils": "0.75.219", "@stacksjs/validation": "0.75.219", "ajv": "^8.20.0", "ajv-formats": "^3.0.1", "bun-plugin-stx": "^0.2.534", "ts-pantry": "^0.11.69" }, "bin": { "bud": "dist/cli.js", "buddy": "dist/cli.js", "stacks": "dist/cli.js" } }, "sha512-zRPDS1/ejvM6CMWLlUmlasoRXbyJgVPqazZpqb/EW6z/kzFdN314FKF2h+UQbBvifkkDhPNPg+Y1b7HyisrAzA=="], | |
| 66 | "@stacksjs/buddy": ["@stacksjs/buddy@0.75.221", "", { "dependencies": { "@stacksjs/actions": "0.75.221", "@stacksjs/ai": "0.75.221", "@stacksjs/alias": "0.75.221", "@stacksjs/analytics": "0.75.221", "@stacksjs/api": "0.75.221", "@stacksjs/arrays": "0.75.221", "@stacksjs/auth": "0.75.221", "@stacksjs/browser-extension": "0.75.221", "@stacksjs/build": "0.75.221", "@stacksjs/bumpx": "^0.2.21", "@stacksjs/cache": "0.75.221", "@stacksjs/chat": "0.75.221", "@stacksjs/clapp": "^0.2.16", "@stacksjs/cli": "0.75.221", "@stacksjs/cloud": "0.75.221", "@stacksjs/cms": "0.75.221", "@stacksjs/collections": "0.75.221", "@stacksjs/commerce": "0.75.221", "@stacksjs/config": "0.75.221", "@stacksjs/database": "0.75.221", "@stacksjs/desktop-build": "0.75.221", "@stacksjs/dns": "0.75.221", "@stacksjs/dnsx": "^0.2.3", "@stacksjs/email": "0.75.221", "@stacksjs/enums": "0.75.221", "@stacksjs/env": "0.75.221", "@stacksjs/error-handling": "0.75.221", "@stacksjs/events": "0.75.221", "@stacksjs/features": "0.75.221", "@stacksjs/git": "0.75.221", "@stacksjs/gitit": "^0.2.5", "@stacksjs/health": "0.75.221", "@stacksjs/httx": "^0.1.10", "@stacksjs/image": "0.75.221", "@stacksjs/lint": "0.75.221", "@stacksjs/logging": "0.75.221", "@stacksjs/notifications": "0.75.221", "@stacksjs/objects": "0.75.221", "@stacksjs/orm": "0.75.221", "@stacksjs/path": "0.75.221", "@stacksjs/payments": "0.75.221", "@stacksjs/realtime": "0.75.221", "@stacksjs/router": "0.75.221", "@stacksjs/rpx": "^0.11.54", "@stacksjs/scheduler": "0.75.221", "@stacksjs/search-engine": "0.75.221", "@stacksjs/security": "0.75.221", "@stacksjs/server": "0.75.221", "@stacksjs/sites": "0.75.221", "@stacksjs/skills": "0.75.221", "@stacksjs/socials": "0.75.221", "@stacksjs/storage": "0.75.221", "@stacksjs/strings": "0.75.221", "@stacksjs/stx": "^0.2.534", "@stacksjs/testing": "0.75.221", "@stacksjs/tinker": "0.75.221", "@stacksjs/tlsx": "^0.13.19", "@stacksjs/ts-cloud": "^0.16.58", "@stacksjs/tunnel": "0.75.221", "@stacksjs/types": "0.75.221", "@stacksjs/ui": "0.75.221", "@stacksjs/utils": "0.75.221", "@stacksjs/validation": "0.75.221", "ajv": "^8.20.0", "ajv-formats": "^3.0.1", "bun-plugin-stx": "^0.2.534", "ts-pantry": "^0.11.69" }, "bin": { "bud": "dist/cli.js", "buddy": "dist/cli.js", "stacks": "dist/cli.js" } }, "sha512-JC1wKFiaL0juh+dPi38WhiHuuz2DMdRtHs/Od3bPrxr5SuMpfrK3QbCHkis3lSa/ZoeZBm7MympRA32LJJb1jA=="], | |
| 67 | 67 | |
| 68 | "@stacksjs/build": ["@stacksjs/build@0.75.219", "", { "dependencies": { "@stacksjs/cli": "0.75.219", "@stacksjs/path": "0.75.219", "@stacksjs/storage": "0.75.219", "bun-plugin-dtsx": "^0.11.19" } }, "sha512-XiUrdrPUqr/hqCpm3RtCmB92LcW6fMils7j/9Vp0EKKxlClOa7zD+sNT1NdoMQ7kVSFIlRhrHfJRskESNpxaxw=="], | |
| 68 | "@stacksjs/build": ["@stacksjs/build@0.75.221", "", { "dependencies": { "@stacksjs/cli": "0.75.221", "@stacksjs/path": "0.75.221", "@stacksjs/storage": "0.75.221", "bun-plugin-dtsx": "^0.11.19" } }, "sha512-yVCTP7WZsaCM4MeooFqsU9eqGhF4j2C3fXyvWNKmisfxED5NZtNcVMbI3gNXkCDbqDk1PWNjwxl12HU0nKwmqg=="], | |
| 69 | 69 | |
| 70 | 70 | "@stacksjs/bumpx": ["@stacksjs/bumpx@0.2.21", "", { "dependencies": { "@stacksjs/clapp": "^0.2.0", "@stacksjs/logsmith": "^0.2.6", "bunfig": "^0.15.6" }, "bin": { "bumpx": "./dist/bin/cli.js" } }, "sha512-OFbqVX35ksAMlRQ7cEUltEqmA0zoBQdK4Zv0Me5FBlFQDt1JIo7K+uE9zCLIry4dyNa7MoKxCeGdGhH4iJbtQA=="], |
| 71 | 71 | |
| @@ -75,161 +75,161 @@ | ||
| 75 | 75 | |
| 76 | 76 | "@stacksjs/bunpress": ["@stacksjs/bunpress@0.2.16", "", { "dependencies": { "@cwcss/crosswind": "^0.2.16", "@stacksjs/clapp": "^0.2.0", "@stacksjs/stx": "^0.2.375", "@stacksjs/ts-i18n": "^0.1.11", "bunfig": "^0.15.17", "ts-syntax-highlighter": "^0.2.14" }, "peerDependencies": { "@stacksjs/ts-cloud": "^0.7.51" }, "optionalPeers": ["@stacksjs/ts-cloud"], "bin": { "bunpress": "./dist/bin/cli.js" } }, "sha512-aHXXkoYvbTirOFqGqUcQOmlv07mAds4bR/oDKbLB1GqvR8tmjdopHugyir6IWzz9JX+3PpGzCG41HNM7l1UZFw=="], |
| 77 | 77 | |
| 78 | "@stacksjs/cache": ["@stacksjs/cache@0.75.219", "", { "dependencies": { "@stacksjs/ts-cache": "^0.1.7" } }, "sha512-JV7Q0VKztLS7CcZ1o9PGcgBwlkXACvIeZZ+nJIwem8uLpWC1HtoQXeM3FJ3EIqz+wnb3e9XliDEir90akZjx8Q=="], | |
| 78 | "@stacksjs/cache": ["@stacksjs/cache@0.75.221", "", { "dependencies": { "@stacksjs/ts-cache": "^0.1.7" } }, "sha512-D4B2tkif9FISL+5xaMff1Ceu2A1iSlMPLyLbgHGzWuqp2mR8KoB/8Ilpk5uq0Bd/xTf63U9opxkQvKlOr1jkTA=="], | |
| 79 | 79 | |
| 80 | "@stacksjs/calendar-api": ["@stacksjs/calendar-api@0.75.219", "", { "dependencies": { "@stacksjs/datetime": "0.75.219", "@stacksjs/stx": "^0.2.534" } }, "sha512-dLOXgtxomxJ1jG6mgm+fBloYcK9OQoTcQ0IS77J+mSMdJgCW5Vf2nVvrhCAie8PYj8Mh2R+buqCNV7yebMXQuw=="], | |
| 80 | "@stacksjs/calendar-api": ["@stacksjs/calendar-api@0.75.221", "", { "dependencies": { "@stacksjs/datetime": "0.75.221", "@stacksjs/stx": "^0.2.534" } }, "sha512-jGOPEJRsPhWMs1ZYFINZSph0nzAihY5ImZOOcngnRpeJY773j0kHLBDqRB4EXIeUrZvj9hAuxCKq/sxqh/85xQ=="], | |
| 81 | 81 | |
| 82 | "@stacksjs/charts": ["@stacksjs/charts@0.75.219", "", { "dependencies": { "@ts-charts/format": "^0.2.7", "@ts-charts/scale": "^0.2.7", "@ts-charts/shape": "^0.2.7" } }, "sha512-JPrAWA9Jgkx+Li3WHjO+GFrmU7TACWBH7Jtn4xCQ+HWdIf5lfBojCr/FVe2G7NWONukU09o27VHJz6H7xbdzaA=="], | |
| 82 | "@stacksjs/charts": ["@stacksjs/charts@0.75.221", "", { "dependencies": { "@ts-charts/format": "^0.2.7", "@ts-charts/scale": "^0.2.7", "@ts-charts/shape": "^0.2.7" } }, "sha512-CGsArz4tiivcGwfX4Ts8yHde0Ow3ujbImnYa1o9634ASl7di25FUwlWFsoCpSV2kJrLgyTO71iXTnTmAlVrP6w=="], | |
| 83 | 83 | |
| 84 | "@stacksjs/chat": ["@stacksjs/chat@0.75.219", "", { "dependencies": { "@stacksjs/config": "0.75.219", "@stacksjs/logging": "0.75.219", "@stacksjs/orm": "0.75.219" } }, "sha512-oDGB0dPgsiQhfrUjISDoCs5BVCs+BqdT6GxVHMNo25iuf6xpkyxkYj1a01CINeOlm22tq/BJPlSi/hE0ca5l6Q=="], | |
| 84 | "@stacksjs/chat": ["@stacksjs/chat@0.75.221", "", { "dependencies": { "@stacksjs/config": "0.75.221", "@stacksjs/logging": "0.75.221", "@stacksjs/orm": "0.75.221" } }, "sha512-VXYmgWxNwwBKTVmhFvb3DgeNAwfxD/+7JVhi9NE7h7b8fA7by6LpcmW+kni2o7aX7Y0Rfeenbhj7rJMkffxoxQ=="], | |
| 85 | 85 | |
| 86 | 86 | "@stacksjs/clapp": ["@stacksjs/clapp@0.2.16", "", {}, "sha512-g/5HUXGxD9QffyPKGdywZODte0DJYwzNhSmrPime1z7TLxWPodJ9IYfIwr2G6Wco+fAQISgaVQUtVf0pUNGV0w=="], |
| 87 | 87 | |
| 88 | 88 | "@stacksjs/clarity": ["@stacksjs/clarity@0.3.33", "", { "dependencies": { "bunfig": "^0.15.6" }, "bin": { "clarity": "./dist/bin/cli.js" } }, "sha512-+5GLKnAI/M/z64UfNLLfQqx/kp+frmRY+NZXYwwGwbrkud/xlsEGDWEExdhqOhqEDMWdb1oafw/ubGAPg1PaFw=="], |
| 89 | 89 | |
| 90 | "@stacksjs/cli": ["@stacksjs/cli@0.75.219", "", { "dependencies": { "@stacksjs/clapp": "^0.2.12", "@stacksjs/collections": "0.75.219", "@stacksjs/error-handling": "0.75.219", "@stacksjs/logging": "0.75.219", "@stacksjs/types": "0.75.219" } }, "sha512-dXbndNyMWmlEa0mCZ1xWypb/ot0lhbz8rdBEqvsKYdwNr0RD2dVQRgZyBIn8tPVfoXrSl6GSiJx0HuLmLCidrQ=="], | |
| 90 | "@stacksjs/cli": ["@stacksjs/cli@0.75.221", "", { "dependencies": { "@stacksjs/clapp": "^0.2.12", "@stacksjs/collections": "0.75.221", "@stacksjs/error-handling": "0.75.221", "@stacksjs/logging": "0.75.221", "@stacksjs/types": "0.75.221" } }, "sha512-YoEkcYY13VLa+HjfVaLVoEYeCdEw7NFloypLH68NWS/j88hPLWhRu0mkoZhT0fhJiTKGRxPpfJbbT9wDG0B1YA=="], | |
| 91 | 91 | |
| 92 | "@stacksjs/cloud": ["@stacksjs/cloud@0.75.219", "", { "dependencies": { "@stacksjs/config": "0.75.219", "@stacksjs/error-handling": "0.75.219", "@stacksjs/logging": "0.75.219", "@stacksjs/path": "0.75.219", "@stacksjs/strings": "0.75.219", "@stacksjs/ts-cloud": "^0.16.58", "@stacksjs/ts-xml": "^0.1.2" } }, "sha512-uDJnIilDp+0AmcQ/wAaIYCsuLLeELJDuW9cIWmHkmZigH56P1+JrXr8lueAZNhx6LgaKLviTXSsdDEMRcQf7+Q=="], | |
| 92 | "@stacksjs/cloud": ["@stacksjs/cloud@0.75.221", "", { "dependencies": { "@stacksjs/config": "0.75.221", "@stacksjs/error-handling": "0.75.221", "@stacksjs/logging": "0.75.221", "@stacksjs/path": "0.75.221", "@stacksjs/strings": "0.75.221", "@stacksjs/ts-cloud": "^0.16.58", "@stacksjs/ts-xml": "^0.1.2" } }, "sha512-TxaAv1FOMLIjsXW9oEA31RRyOgjmogkAy18Qce1AG9cVEYX1YqOkLVWRJmcOGw85SgzhfSQJCvdragncW/T7NQ=="], | |
| 93 | 93 | |
| 94 | "@stacksjs/cms": ["@stacksjs/cms@0.75.219", "", { "dependencies": { "@stacksjs/database": "0.75.219", "@stacksjs/error-handling": "0.75.219", "@stacksjs/orm": "0.75.219", "@stacksjs/path": "0.75.219", "@stacksjs/sites": "0.75.219", "@stacksjs/slug": "0.75.219", "@stacksjs/stx": "^0.2.534", "@stacksjs/validation": "0.75.219", "ts-medium-editor": "^0.2.0", "ts-slug": "^0.1.2", "ts-spreadsheets": "^0.2.5" } }, "sha512-BY0fkN8Y4jO/VJiet4KyLDLF1cvz/9ZSTCkOZvPOWU52BQi03NT+MhnaQ3UmvR4lVwvozzChTGXpKUE8l4WElw=="], | |
| 94 | "@stacksjs/cms": ["@stacksjs/cms@0.75.221", "", { "dependencies": { "@stacksjs/database": "0.75.221", "@stacksjs/error-handling": "0.75.221", "@stacksjs/orm": "0.75.221", "@stacksjs/path": "0.75.221", "@stacksjs/sites": "0.75.221", "@stacksjs/slug": "0.75.221", "@stacksjs/stx": "^0.2.534", "@stacksjs/validation": "0.75.221", "ts-medium-editor": "^0.2.0", "ts-slug": "^0.1.2", "ts-spreadsheets": "^0.2.5" } }, "sha512-FDFu7A3ufyBTzBFJtBQgqmbySId4LFUWnrwGn/K4ys6jU3tNL3eO/OrvZ9eyxtw7KtfYZ63IrMJ7xRVyJqIkOw=="], | |
| 95 | 95 | |
| 96 | "@stacksjs/collections": ["@stacksjs/collections@0.75.219", "", { "dependencies": { "ts-collect": "^0.4.3" } }, "sha512-vv+lK82ZvbkvMokY8MTrbspUj+ftHzaK3lBKsX3173i8GWBTJ155zaj+AK1da7Gp65pW1dyHbuI5kJsk1SnctA=="], | |
| 96 | "@stacksjs/collections": ["@stacksjs/collections@0.75.221", "", { "dependencies": { "ts-collect": "^0.4.3" } }, "sha512-PIM59l69FPaWEufFE/wmQi2GszPrgraDSLdfSzaQn71KkfbNesPklAbsUQH5Ia8WuYlpagtW6R/kmUR3xMVnBQ=="], | |
| 97 | 97 | |
| 98 | "@stacksjs/commerce": ["@stacksjs/commerce@0.75.219", "", { "dependencies": { "@stacksjs/cache": "0.75.219", "@stacksjs/config": "0.75.219", "@stacksjs/database": "0.75.219", "@stacksjs/env": "0.75.219", "@stacksjs/error-handling": "0.75.219", "@stacksjs/events": "0.75.219", "@stacksjs/logging": "0.75.219", "@stacksjs/orm": "0.75.219", "@stacksjs/payments": "0.75.219", "@stacksjs/realtime": "0.75.219", "@stacksjs/strings": "0.75.219", "@stacksjs/validation": "0.75.219", "ts-spreadsheets": "^0.2.0" } }, "sha512-qEKbEdd9ZLG7ZDs4pA1LQ3yeBv0I2Vi3KMqx5Xx5eFMXzfw2t+vCDaf/3lMUbFUxJz8Umf54e6AvPzdeT8X9hw=="], | |
| 98 | "@stacksjs/commerce": ["@stacksjs/commerce@0.75.221", "", { "dependencies": { "@stacksjs/cache": "0.75.221", "@stacksjs/config": "0.75.221", "@stacksjs/database": "0.75.221", "@stacksjs/env": "0.75.221", "@stacksjs/error-handling": "0.75.221", "@stacksjs/events": "0.75.221", "@stacksjs/logging": "0.75.221", "@stacksjs/orm": "0.75.221", "@stacksjs/payments": "0.75.221", "@stacksjs/realtime": "0.75.221", "@stacksjs/strings": "0.75.221", "@stacksjs/validation": "0.75.221", "ts-spreadsheets": "^0.2.0" } }, "sha512-EXsaTWQorx4vEQAbjBDfO6nRy/VKC1GYxoR1E+rBaXA4x4B7NVi73P4e3VdFPFJXJJ6f3PMXlEoDp46YFZ0sqw=="], | |
| 99 | 99 | |
| 100 | 100 | "@stacksjs/components": ["@stacksjs/components@0.2.556", "", { "dependencies": { "@iconify-json/f7": "^1.2.2", "@stacksjs/stx": "0.2.556", "@stacksjs/ts-css": "^0.3.11", "bun-plugin-stx": "0.2.556", "ts-syntax-highlighter": "^0.2.17", "ts-video-player": "^0.1.9" } }, "sha512-2InQNlRHa62ke6yaDQI1NKME+ayVwwNqx6ZkITyKQ0ldfHOyjjvTobTr3uxhPmHqdZ326Y+ikAI4C/Loywj/WQ=="], |
| 101 | 101 | |
| 102 | "@stacksjs/composables": ["@stacksjs/composables@0.75.219", "", { "dependencies": { "@stacksjs/datetime": "0.75.219", "@stacksjs/stx": "^0.2.534" } }, "sha512-8fXtgEFRf4Y5AqKL56W4MEntq/zX16rLXPdy4tA1+nJa0yuaTr1uHOp6V3pfG5Oy/w2AZsAbFdp3yRjGZx31Kw=="], | |
| 102 | "@stacksjs/composables": ["@stacksjs/composables@0.75.221", "", { "dependencies": { "@stacksjs/datetime": "0.75.221", "@stacksjs/stx": "^0.2.534" } }, "sha512-YtOzZWrirlwdT3c8uidMu8UKqLvD2h3ZGCI6fekYx8aJC8PGkciDBJAlt8t5Uo1lgADvoH9J8hwu1kBJalSmFw=="], | |
| 103 | 103 | |
| 104 | "@stacksjs/config": ["@stacksjs/config@0.75.219", "", { "dependencies": { "@stacksjs/events": "0.75.219", "@stacksjs/features": "0.75.219", "@stacksjs/path": "0.75.219", "@stacksjs/tunnel": "0.75.219", "ts-pantry": "^0.11.69" } }, "sha512-Xgbn45jteS6/kMexeiNQb8D6FKAf9/AjDxKiWn/eKf4KjnMy/s+ZMsg6uvMDrgKcA3WmkJEWs4QJh6456tyWwg=="], | |
| 104 | "@stacksjs/config": ["@stacksjs/config@0.75.221", "", { "dependencies": { "@stacksjs/events": "0.75.221", "@stacksjs/features": "0.75.221", "@stacksjs/path": "0.75.221", "@stacksjs/tunnel": "0.75.221", "ts-pantry": "^0.11.69" } }, "sha512-c3h0+/VvqseJQjOM+OBW8paXgxeocAgUKptVs99wSUQzG4z+YhhT0uBL+Ixk0ZORXdFELefq7icZgiFwv689cQ=="], | |
| 105 | 105 | |
| 106 | "@stacksjs/cron": ["@stacksjs/cron@0.75.219", "", {}, "sha512-2MHBbTF0lVlvSsl1kvyUiBFc7w+EClzJntG4YKE/JWiIQPAbk67FRUGbP4Fg+0aP206fDi+XCsroBIn4/AYmVQ=="], | |
| 106 | "@stacksjs/cron": ["@stacksjs/cron@0.75.221", "", {}, "sha512-+zjYLMzEVi2KPOi2KHVaxzTG5h3m/rRxKt8q2LwuE0T2lNxHlWUojesukMoCUMQf+wP1NrflkpsnZ/rYyrXZHw=="], | |
| 107 | 107 | |
| 108 | "@stacksjs/database": ["@stacksjs/database@0.75.219", "", { "dependencies": { "@stacksjs/config": "0.75.219", "@stacksjs/env": "0.75.219", "@stacksjs/error-handling": "0.75.219", "@stacksjs/faker": "0.75.219", "@stacksjs/features": "0.75.219", "@stacksjs/logging": "0.75.219", "@stacksjs/model-meta": "0.75.219", "@stacksjs/path": "0.75.219", "@stacksjs/query-builder": "0.75.219", "@stacksjs/security": "0.75.219", "@stacksjs/storage": "0.75.219", "@stacksjs/strings": "0.75.219", "@stacksjs/ts-validation": "^0.5.6", "bun-query-builder": "^0.3.9", "dynamodb-tooling": "^0.3.6" } }, "sha512-6Q5WAwIgbHLwLoBstUCvxrk4X49TbLLJyeDC/TmkyCkRPCgLdcymOi39Jvs1wpik54lna18rGSVaJ1ByPOJYKg=="], | |
| 108 | "@stacksjs/database": ["@stacksjs/database@0.75.221", "", { "dependencies": { "@stacksjs/config": "0.75.221", "@stacksjs/env": "0.75.221", "@stacksjs/error-handling": "0.75.221", "@stacksjs/faker": "0.75.221", "@stacksjs/features": "0.75.221", "@stacksjs/logging": "0.75.221", "@stacksjs/model-meta": "0.75.221", "@stacksjs/path": "0.75.221", "@stacksjs/query-builder": "0.75.221", "@stacksjs/security": "0.75.221", "@stacksjs/storage": "0.75.221", "@stacksjs/strings": "0.75.221", "@stacksjs/ts-validation": "^0.5.6", "bun-query-builder": "^0.3.9", "dynamodb-tooling": "^0.3.6" } }, "sha512-3gqYnz5qHVeI07oeiiDSAjuaMqu/iYzMZQ/HNyOGiCGPF+Kp93xULYYYddXt2R2XW3NCZ1PXihNJZBrLxrEtcQ=="], | |
| 109 | 109 | |
| 110 | "@stacksjs/datetime": ["@stacksjs/datetime@0.75.219", "", {}, "sha512-WFpz4OP6w6UbjhpfV0hvElhuHSUWO9rJL1xuoyW3CpdBRLu7RsWvWudMsBklwjAiuZ0QiWV4Lpw1v/oIW4UJnQ=="], | |
| 110 | "@stacksjs/datetime": ["@stacksjs/datetime@0.75.221", "", {}, "sha512-qL9t6S9ykomv1tSplSQ/3SVPw7GYBB+pR0QLeu+0F8dwCPEeldq8/revXigsL4hn1k8syffVQ9zLERrNUy+lDw=="], | |
| 111 | 111 | |
| 112 | "@stacksjs/defaults": ["@stacksjs/defaults@0.75.219", "", { "dependencies": { "@iconify-json/f7": "^1.2.2", "@iconify-json/hugeicons": "^1.2.35", "@stacksjs/mobile": "0.75.219", "@stacksjs/sanitizer": "^0.2.534", "@xterm/addon-fit": "^0.11.0", "@xterm/xterm": "^6.0.0", "ts-qr-codes": "^0.1.8" } }, "sha512-wrw5pbvoB5ZMRrQWE2kdZ6lNhYABkoYN856oRzuvpYYNMPiDGzXRbfx8F8xGBFZLBxsJUvEBGBsEhn7X+ihKSw=="], | |
| 112 | "@stacksjs/defaults": ["@stacksjs/defaults@0.75.221", "", { "dependencies": { "@iconify-json/f7": "^1.2.2", "@iconify-json/hugeicons": "^1.2.35", "@stacksjs/mobile": "0.75.221", "@stacksjs/sanitizer": "^0.2.534", "@xterm/addon-fit": "^0.11.0", "@xterm/xterm": "^6.0.0", "ts-qr-codes": "^0.1.8" } }, "sha512-/q67bbK8Dwdo6/1kqt+VLBZz8OsZQCTLGVTatVwghCfR2tMTJXHp8RvwN+vYKrDu0O6MXgkj2x9580BJXSZ84g=="], | |
| 113 | 113 | |
| 114 | "@stacksjs/desktop": ["@stacksjs/desktop@0.2.563", "", { "dependencies": { "craft-native": ">=0.0.111" } }, "sha512-kNN1D1CZVF+luzgXhk12iRcQM51zrZy4siHhQ7y3WTDQBKY4O8sW+j/KrXPgMYaf1dDc9O3moxBSFPvPvqmuOg=="], | |
| 114 | "@stacksjs/desktop": ["@stacksjs/desktop@0.2.573", "", { "dependencies": { "craft-native": ">=0.0.111" } }, "sha512-2QIG4rm8AJ8ORCUnYjcJ6hNA/p6iXIFhjrjv3aM2hmB/7bs1juDVIheVuSD6+HaWrVsRLWlmRqL9AkdsQA5tLg=="], | |
| 115 | 115 | |
| 116 | "@stacksjs/desktop-build": ["@stacksjs/desktop-build@0.75.219", "", { "dependencies": { "craft-native": ">=0.0.135" } }, "sha512-MI/gpXP/vY2/X2lNW0Yr5W6HLrf52K8TGAGUY7Y7PBcWS3ixDyAkpKBLJBsAIM8bIYg6N3sqxLNKLs9PJCWEHg=="], | |
| 116 | "@stacksjs/desktop-build": ["@stacksjs/desktop-build@0.75.221", "", { "dependencies": { "craft-native": ">=0.0.135" } }, "sha512-a6gKMYiR0sdICDKhp81Vl2n9AHQXXPhAebYlg8gwcP7t6+9u6cG+ht9IUC1fL5nK2BNH9P9PyLfRA+NjXdlzZA=="], | |
| 117 | 117 | |
| 118 | "@stacksjs/dns": ["@stacksjs/dns@0.75.219", "", { "dependencies": { "@stacksjs/action-runner": "0.75.219", "@stacksjs/config": "0.75.219", "@stacksjs/dnsx": "^0.2.3", "@stacksjs/enums": "0.75.219", "@stacksjs/error-handling": "0.75.219", "@stacksjs/logging": "0.75.219", "@stacksjs/path": "0.75.219", "@stacksjs/storage": "0.75.219", "@stacksjs/ts-cloud": "^0.16.58" } }, "sha512-4C8OWXGaN4332aOXfKMyOnLvy56isHT+Opdt6r+EQ49W7gsneot1ONw3G6RVGGo3T6/08prHaFbiHvVGozujRQ=="], | |
| 118 | "@stacksjs/dns": ["@stacksjs/dns@0.75.221", "", { "dependencies": { "@stacksjs/action-runner": "0.75.221", "@stacksjs/config": "0.75.221", "@stacksjs/dnsx": "^0.2.3", "@stacksjs/enums": "0.75.221", "@stacksjs/error-handling": "0.75.221", "@stacksjs/logging": "0.75.221", "@stacksjs/path": "0.75.221", "@stacksjs/storage": "0.75.221", "@stacksjs/ts-cloud": "^0.16.58" } }, "sha512-CvTSkGiqY654cqNGRD0ab0Fo70tLzhV5QoXjEvQydksc2kXZo+KwXmsQrlqx033yHkjzD2HtqW4x1T7f07FG1Q=="], | |
| 119 | 119 | |
| 120 | 120 | "@stacksjs/dnsx": ["@stacksjs/dnsx@0.2.3", "", { "bin": { "dnsx": "./dist/bin/cli.js" } }, "sha512-KoubPLapgNSEOoTJNEL3BV1WIhSHz70Kzyf2i2EM9ZViaNOeHrUOIwG8qRX9D+L/5Njh45LmAG+vwkaJalD1ng=="], |
| 121 | 121 | |
| 122 | "@stacksjs/docs": ["@stacksjs/docs@0.75.219", "", { "dependencies": { "@stacksjs/bunpress": "^0.2.15", "@stacksjs/config": "0.75.219", "@stacksjs/path": "0.75.219" } }, "sha512-jmbX4nTZ35Z0ICHImZxr50lUvbblxhJtuWMVRMKHgvaFWXOOxGMhV7Tr+Y1Vcgz1N7iLIoPEuQbwy69DA+0s4w=="], | |
| 122 | "@stacksjs/docs": ["@stacksjs/docs@0.75.221", "", { "dependencies": { "@stacksjs/bunpress": "^0.2.15", "@stacksjs/config": "0.75.221", "@stacksjs/path": "0.75.221" } }, "sha512-dpq/bMwwvI3LdCAybolApHRQ3A0pXt5Fv9tTa6ingAPHfpdzEv89aQhGxNSh5pEB/U35o6j10dL1bZ9/3Fes4Q=="], | |
| 123 | 123 | |
| 124 | 124 | "@stacksjs/dtsx": ["@stacksjs/dtsx@0.11.19", "", { "dependencies": { "@stacksjs/clapp": "^0.2.16" }, "bin": { "dtsx": "./dist/bin/cli.js" } }, "sha512-QPf4h2gGRW9kyLXpIAMkYEZkmVkoFjlWK0jwWKpc5ItrqpvyMx2gqYBqa0qVij43oMBo2QiQTrqW4+iYEho6Tg=="], |
| 125 | 125 | |
| 126 | "@stacksjs/email": ["@stacksjs/email@0.75.219", "", { "dependencies": { "@stacksjs/config": "0.75.219", "@stacksjs/database": "0.75.219", "@stacksjs/events": "0.75.219", "@stacksjs/logging": "0.75.219", "@stacksjs/mail": "^0.3.11", "@stacksjs/path": "0.75.219", "@stacksjs/storage": "0.75.219", "@stacksjs/strings": "0.75.219", "@stacksjs/stx": "^0.2.534", "@stacksjs/ts-cloud": "^0.16.58", "@stacksjs/utils": "0.75.219" }, "peerDependencies": { "@stacksjs/queue": "0.75.219" }, "optionalPeers": ["@stacksjs/queue"] }, "sha512-SmcSU/AAXyqal0mZUCAhVWY4Omw47irJZEzYNuu3Onp1l7IJl8uSNgt/C8DyKAtZNi1fhut3GcwIaNGNaLBoWA=="], | |
| 126 | "@stacksjs/email": ["@stacksjs/email@0.75.221", "", { "dependencies": { "@stacksjs/config": "0.75.221", "@stacksjs/database": "0.75.221", "@stacksjs/events": "0.75.221", "@stacksjs/logging": "0.75.221", "@stacksjs/mail": "^0.3.11", "@stacksjs/path": "0.75.221", "@stacksjs/storage": "0.75.221", "@stacksjs/strings": "0.75.221", "@stacksjs/stx": "^0.2.534", "@stacksjs/ts-cloud": "^0.16.58", "@stacksjs/utils": "0.75.221" }, "peerDependencies": { "@stacksjs/queue": "0.75.221" }, "optionalPeers": ["@stacksjs/queue"] }, "sha512-Fiw/cp+RVwmSV3sJ6UYeJY6MQKyLqoqofV2EokOlgtMoRHEPMtFdCUcOZTv+bFVa1ze1XS09oPLSZpmHtJ5qng=="], | |
| 127 | 127 | |
| 128 | "@stacksjs/enums": ["@stacksjs/enums@0.75.219", "", {}, "sha512-Sy2K+I5mcV44vya9EyD7LA5BxvwcUCsiMoobGK6owyGnYPn0b4flwmmJbbpCX9rQcdjSvNTHYvLNZMXB8PFjVw=="], | |
| 128 | "@stacksjs/enums": ["@stacksjs/enums@0.75.221", "", {}, "sha512-4UOaJ0I6C9I0eY8QvVqj8Zva+VosPiuyAr2g+5o9MjOazGCgmM8jamiwOLDQ7gTn3YS11mxQXIQE1xff3/ilEg=="], | |
| 129 | 129 | |
| 130 | "@stacksjs/env": ["@stacksjs/env@0.75.219", "", { "dependencies": { "@stacksjs/path": "0.75.219" } }, "sha512-xwKILq4muUxBjvjkL+L/3q0V4A0tR9mUZDWyKte4Nwo8RK0wqmZUH2kwGFrpm9YY1eUAZKibFgBA9S5X6niVVg=="], | |
| 130 | "@stacksjs/env": ["@stacksjs/env@0.75.221", "", { "dependencies": { "@stacksjs/path": "0.75.221" } }, "sha512-vS4JObosctiPkXtom87BjXtWZ01zn4+tUyfT02Is5Gl+tOn6DqzeBgOfZwnsyb068J/cfxhFwpPkbSO/MLXqQg=="], | |
| 131 | 131 | |
| 132 | "@stacksjs/error-handling": ["@stacksjs/error-handling@0.75.219", "", { "dependencies": { "@bughq/sdk": "^0.3.0", "@stacksjs/path": "0.75.219", "@stacksjs/stx": "^0.2.534", "@stacksjs/types": "0.75.219", "ts-error-handling": "^0.1.3", "ts-syntax-highlighter": "^0.2.17" } }, "sha512-nLooxnAR39Drf2NtNqxTkur7TpOIwthbkeEIRW3RM4+iViZJcocueT48ZAzzSacfA/9HA86te2LRUXpaObXmbg=="], | |
| 132 | "@stacksjs/error-handling": ["@stacksjs/error-handling@0.75.221", "", { "dependencies": { "@bughq/sdk": "^0.3.0", "@stacksjs/path": "0.75.221", "@stacksjs/stx": "^0.2.534", "@stacksjs/types": "0.75.221", "ts-error-handling": "^0.1.3", "ts-syntax-highlighter": "^0.2.17" } }, "sha512-PrAD9DhMuaZm0wvmags2N7fktjEfhmcaWWj6D9cSnvdLajp9ypJGqtE1Y7asbDnMl/9ClcX9/OGDfFvu8wDSJQ=="], | |
| 133 | 133 | |
| 134 | "@stacksjs/events": ["@stacksjs/events@0.75.219", "", { "dependencies": { "@stacksjs/path": "0.75.219" } }, "sha512-0xOXhSeDSJkgsogNv/ALP+k+NH3H/tW8owfakESqzp6aKIqQIt4MPCWPhijs/Pu1d9IjrF4vWTNxIfg5RqZW1g=="], | |
| 134 | "@stacksjs/events": ["@stacksjs/events@0.75.221", "", { "dependencies": { "@stacksjs/path": "0.75.221" } }, "sha512-pZ9SV3k52Zoj+Kc3h/IpvOgjbsOmRSYKelKBXup2bWkInYzygEKJcX/qc3R6X++abS2Ka26Lrr3hHNgeiVZisA=="], | |
| 135 | 135 | |
| 136 | "@stacksjs/faker": ["@stacksjs/faker@0.75.219", "", { "dependencies": { "@stacksjs/ts-faker": "^0.1.8" } }, "sha512-5JdZv4sL4RxkgqgMtf8vEEWAA4sdb+Sgb6sNlx+yz0fnTdBJ1aVDObdM674m7B2bLOFysh9cNSnZF6ROsLci3A=="], | |
| 136 | "@stacksjs/faker": ["@stacksjs/faker@0.75.221", "", { "dependencies": { "@stacksjs/ts-faker": "^0.1.8" } }, "sha512-raS69LvnuaX7cXonw3O/MkrML9EbIF//xm/mtO9NFtiv2jjRL3HggnRP/yWjNEWxT3VOZ57caqRlGFrkaSGn4Q=="], | |
| 137 | 137 | |
| 138 | "@stacksjs/feature-flags": ["@stacksjs/feature-flags@0.75.219", "", { "dependencies": { "@stacksjs/config": "0.75.219", "@stacksjs/database": "0.75.219" } }, "sha512-brajSdfZg5/L4UbEI8LD+JIDzsIr37F1aftOioiz8FstoRm2fg6eXz0ys+3PpV0JkmtZzA2Gb7ZqxduqMSn+Wg=="], | |
| 138 | "@stacksjs/feature-flags": ["@stacksjs/feature-flags@0.75.221", "", { "dependencies": { "@stacksjs/config": "0.75.221", "@stacksjs/database": "0.75.221" } }, "sha512-UFLPwSCQOiOS7K+1FzKyqfg4nX6nqsQJb6iqmzH62aQUMuqusQUtHWNdGQczgPnS24ywJoTwgKqJwv+JiT0uOQ=="], | |
| 139 | 139 | |
| 140 | "@stacksjs/features": ["@stacksjs/features@0.75.219", "", { "dependencies": { "@stacksjs/path": "0.75.219" } }, "sha512-zZpgEmYf6E8aJlcvXxgxn0ZlyLg5EWw7ZFmuUHj85rLEgGhU90jKVrFHaitdR6KWz9eO9+fsAqDEhhTmyanIvA=="], | |
| 140 | "@stacksjs/features": ["@stacksjs/features@0.75.221", "", { "dependencies": { "@stacksjs/path": "0.75.221" } }, "sha512-KAVZjBkYnFvO2TlnlE+a9ygvgf9BSccLK4zspHaDz3ESwwd7bT1VUPLPUEEbo/sSQNza8/fGfB+rdQziGT/yZA=="], | |
| 141 | 141 | |
| 142 | "@stacksjs/forms": ["@stacksjs/forms@0.75.219", "", { "dependencies": { "@stacksjs/config": "0.75.219", "@stacksjs/database": "0.75.219", "@stacksjs/logging": "0.75.219", "@stacksjs/notifications": "0.75.219" } }, "sha512-5MqO5rwvPbHClTfst7Ah3Lk/G9PL2HqqA1mYdt3ioIIRDqUSCLUwZPgsqvsAOGo1+S3RK1fsxedTDq39zgpsvA=="], | |
| 142 | "@stacksjs/forms": ["@stacksjs/forms@0.75.221", "", { "dependencies": { "@stacksjs/config": "0.75.221", "@stacksjs/database": "0.75.221", "@stacksjs/logging": "0.75.221", "@stacksjs/notifications": "0.75.221" } }, "sha512-rjwiAqqAyM4NPwm4himRUYQVbIZgzYuopvGlezjjNFEYC/PHai7T8az8ZQsvCZTi74JzdMJnp3R/xoM18VCP8A=="], | |
| 143 | 143 | |
| 144 | "@stacksjs/git": ["@stacksjs/git@0.75.219", "", { "dependencies": { "cz-git": "^1.14.0" } }, "sha512-8881M2+HhAM5rLgxO9ONPtnfIFH8BpU53/i0VBTiJIQO2uCnes0VYKyjytuyObwkOlJ0F1JjT9m9DelFYol8WA=="], | |
| 144 | "@stacksjs/git": ["@stacksjs/git@0.75.221", "", { "dependencies": { "cz-git": "^1.14.0" } }, "sha512-xMdsVyQwMQNXoGqNd5AQmJgjlGrV7znYH9yaJa7JeLYNePHO7iiqlLpS6sF/3K9Ae6pVVrYGm7e4IMBNGXrn0Q=="], | |
| 145 | 145 | |
| 146 | "@stacksjs/github": ["@stacksjs/github@0.75.219", "", {}, "sha512-LSPc1QbPsgzdahTREmZ4HVIllxenEP2BK02hmmilQ0FOPUhIChIOQcCo8jruzNmO+jv4ycN9tgXb4y59GP/DBQ=="], | |
| 146 | "@stacksjs/github": ["@stacksjs/github@0.75.221", "", {}, "sha512-jqxl74x6i+Wpp7CA16gIQcoSdjkznW9iZCQjxEOGJ0x8joVmcvrH6APxQAoZXklclzuYHqZNyNDhzJsKfLm0aA=="], | |
| 147 | 147 | |
| 148 | 148 | "@stacksjs/gitit": ["@stacksjs/gitit@0.2.5", "", { "bin": { "gitit": "dist/bin/cli.js" } }, "sha512-HI9L1EsWTXG4JxqMlEjujDWovH5UnYqP6Uv9Pll74+vLGEH2OK6FRfarNQgRHBkMyzBx2fjutAHOXrPSCTN8eQ=="], |
| 149 | 149 | |
| 150 | "@stacksjs/health": ["@stacksjs/health@0.75.219", "", {}, "sha512-nMMTPZkRFkUbEpOUW1KjDUYsqo6MGkdWkboExkjg30NGpN1OP1OicP7tKC7HlQc5GG0C+QXXhazaFnY+HqYGzg=="], | |
| 150 | "@stacksjs/health": ["@stacksjs/health@0.75.221", "", {}, "sha512-CApjb2B/2aPcGFpYsaUH1MDZiWcHAw7xPrC7EukGBxBV51aRO5mxGv4wSLXyoWw31GTx2QEeYLzKS2LJB843ZQ=="], | |
| 151 | 151 | |
| 152 | "@stacksjs/http": ["@stacksjs/http@0.75.219", "", { "dependencies": { "@stacksjs/httx": "^0.1.11" } }, "sha512-nqNWbXTqkWugDW4/Tq44hXxXtO4oyEMoyVaXINtRhGw5rSNiliYe38dcAAGz3X45rm68mlrVc/Pif70sfGTJfA=="], | |
| 152 | "@stacksjs/http": ["@stacksjs/http@0.75.221", "", { "dependencies": { "@stacksjs/httx": "^0.1.11" } }, "sha512-bbwX6S/13XvBs6uKcELR6iQeFanIVWFJR77CVmq3g6fpryvS/S4sF78L2DPGRWkO8mPR+krR/4ErcWvmTNVQFw=="], | |
| 153 | 153 | |
| 154 | 154 | "@stacksjs/httx": ["@stacksjs/httx@0.1.11", "", { "dependencies": { "ts-error-handling": "^0.1.0" }, "bin": { "httx": "./dist/bin/cli.js", "http": "./dist/bin/cli.js", "https": "./dist/bin/cli.js" } }, "sha512-7Ml5D1NpTl4qadeNNFh0CFXjNnv4rb1+9d2MC159FElVo3SbAGWGpobSnwrjg/SZK10rc9mpeOU3l2LPqvJ9uQ=="], |
| 155 | 155 | |
| 156 | "@stacksjs/i18n": ["@stacksjs/i18n@0.75.219", "", { "dependencies": { "@stacksjs/config": "0.75.219", "@stacksjs/logging": "0.75.219", "@stacksjs/path": "0.75.219", "@stacksjs/ts-i18n": "^0.1.11" } }, "sha512-AOl559zpXtEHxlxDX3RPjhEoaAT0Y8mC1Rv8la5LlXZd1346+G7E69i4Agk9/7BG5s6Vn+1IGYtLTDHOjleUdQ=="], | |
| 156 | "@stacksjs/i18n": ["@stacksjs/i18n@0.75.221", "", { "dependencies": { "@stacksjs/config": "0.75.221", "@stacksjs/logging": "0.75.221", "@stacksjs/path": "0.75.221", "@stacksjs/ts-i18n": "^0.1.11" } }, "sha512-hhtvSuU9JxtjJr9OfILl9reB5TNmWkdW9jvctCXshY0S5c+08/y8B/6Fk7rdQ366ysivh+zUQo+dSVix1FpHCA=="], | |
| 157 | 157 | |
| 158 | "@stacksjs/image": ["@stacksjs/image@0.75.219", "", { "dependencies": { "@stacksjs/ts-svg": "^0.1.5", "@stacksjs/types": "0.75.219", "ts-images": "^0.2.23" } }, "sha512-h8xKthKs49CH8pvZmXbEBq0r8O66rP+Iy9EsyTebeAn59VBnk7EFnQ1fZ3fnezW387zNjAjael2SiIrp+yo7fA=="], | |
| 158 | "@stacksjs/image": ["@stacksjs/image@0.75.221", "", { "dependencies": { "@stacksjs/ts-svg": "^0.1.5", "@stacksjs/types": "0.75.221", "ts-images": "^0.2.23" } }, "sha512-k9jPEl/437y35Eeko9bqcnq5IHXWEe+WDfDewjX5HlX37fTxhFpJuBzfYYmIjCwwWrxicgVu3mqSw/dEe+UYaA=="], | |
| 159 | 159 | |
| 160 | "@stacksjs/lint": ["@stacksjs/lint@0.75.219", "", { "dependencies": { "pickier": "^0.1.70" } }, "sha512-Q+9GMPrA9Xd63TtD5UwqyNjhNxCiyPbg+I4Mo+6KetWD2A/xqReetuAAkSnIdJxTn8WLyOGGW+8P/39s7gc/Tw=="], | |
| 160 | "@stacksjs/lint": ["@stacksjs/lint@0.75.221", "", { "dependencies": { "pickier": "^0.1.70" } }, "sha512-KCBi1FkxAJBtC8l5vlQAENsNgZvp5fV0oLozoR6wRc9Oh71PdbJMjdm+CqEJS/7X/R8dJ+hugy7jYaBdV2Zmzw=="], | |
| 161 | 161 | |
| 162 | "@stacksjs/logging": ["@stacksjs/logging@0.75.219", "", { "dependencies": { "@loghq/stacks": "^0.2.6", "@stacksjs/clarity": "^0.3.33", "@stacksjs/config": "0.75.219", "@stacksjs/error-handling": "0.75.219", "@stacksjs/path": "0.75.219", "@stacksjs/types": "0.75.219" } }, "sha512-txlY3x7VEQHZGGn8OY9fWs3V3U4SeXpi562qjwLHsd3VQEQsIhPsg7KX5MSHdFZC1VBs6dC0UuGftNnuh06AlQ=="], | |
| 162 | "@stacksjs/logging": ["@stacksjs/logging@0.75.221", "", { "dependencies": { "@loghq/stacks": "^0.2.6", "@stacksjs/clarity": "^0.3.33", "@stacksjs/config": "0.75.221", "@stacksjs/error-handling": "0.75.221", "@stacksjs/path": "0.75.221", "@stacksjs/types": "0.75.221" } }, "sha512-b3ygs4yTijiZxb/PFVjPj2xwu3KsLz3ioCt6PUjbtufLr4MpSOxiEuEcyzB4ys8HAbnPL4MiR04ssDrdWjYUkg=="], | |
| 163 | 163 | |
| 164 | 164 | "@stacksjs/logsmith": ["@stacksjs/logsmith@0.2.11", "", { "dependencies": { "@stacksjs/clapp": "^0.2.0", "bunfig": "^0.15.6", "pickier": "^0.1.9" }, "bin": { "@stacksjs/logsmith": "./dist/bin/cli.js", "logsmith": "./dist/bin/cli.js" } }, "sha512-wqqAxewpsHVDzpC+IRy+yYyGXGlv1j5GCqYTh8fL7d3LLhnRPN72PEQywM357burtORvdkFHfOzk3Ih1SMvuiw=="], |
| 165 | 165 | |
| 166 | 166 | "@stacksjs/mail": ["@stacksjs/mail@0.3.12", "", {}, "sha512-RLoGTaS4Hi8Jy5LRrtNsy5038mTOTqALvAUxm2hrqKOOZLWj091/wms0hnBoRQ1bfk0vuQMSYQNlmzoVUeIicA=="], |
| 167 | 167 | |
| 168 | "@stacksjs/mobile": ["@stacksjs/mobile@0.75.219", "", { "dependencies": { "@bughq/sdk": "^0.3.0", "@stacksjs/types": "0.75.219", "craft-native": ">=0.0.179" } }, "sha512-2IYgWSf93DKcIqOx6ieRT45vXWMB7Mt8U4dcq0t/Kmtt7e/GQkhnloG6uU9zP/ZbOzllh91Dz2PnmKSfJ5DK1Q=="], | |
| 168 | "@stacksjs/mobile": ["@stacksjs/mobile@0.75.221", "", { "dependencies": { "@bughq/sdk": "^0.3.0", "@stacksjs/types": "0.75.221", "craft-native": ">=0.0.179" } }, "sha512-PmODrSk5la9korZmKjm4p0jyy05+cswJ4BMN7PDUYT2x19VSha/d2tzwCveW8yfsZx6YTCGJ/zkqR9C1+5LNgA=="], | |
| 169 | 169 | |
| 170 | "@stacksjs/model-meta": ["@stacksjs/model-meta@0.75.219", "", { "dependencies": { "@stacksjs/path": "0.75.219", "@stacksjs/storage": "0.75.219", "@stacksjs/strings": "0.75.219", "@stacksjs/ts-validation": "^0.5.6", "@stacksjs/types": "0.75.219", "@stacksjs/validation": "0.75.219" } }, "sha512-2NHb81xOtUUFx1YokFJ8bsbaLvNcKVdkwGzHrY/yCcH4N+Aeb+bLHy3R2sfihsMko3BEm1V37528PBR+S/bXvw=="], | |
| 170 | "@stacksjs/model-meta": ["@stacksjs/model-meta@0.75.221", "", { "dependencies": { "@stacksjs/path": "0.75.221", "@stacksjs/storage": "0.75.221", "@stacksjs/strings": "0.75.221", "@stacksjs/ts-validation": "^0.5.6", "@stacksjs/types": "0.75.221", "@stacksjs/validation": "0.75.221" } }, "sha512-Wl1luIJ9JcjMBZJlry3xeQIaWLX/0ceM9ifUVw7Z5EF1sOLxcvIKwYOMg5YO9n9F4HIjBmkp9tcfHrNWzAEA8w=="], | |
| 171 | 171 | |
| 172 | "@stacksjs/newsletter": ["@stacksjs/newsletter@0.75.219", "", { "dependencies": { "@stacksjs/database": "0.75.219", "@stacksjs/email": "0.75.219", "@stacksjs/orm": "0.75.219", "@stacksjs/queue": "0.75.219" } }, "sha512-EYyaBR/rQPFPb0hlfdlU/3DWMLWHu2BVTPcbevQsP0M/O5hzZOxL4CoXLWqBIYQmJ4Nql9lD6V6czMh/BdlWBQ=="], | |
| 172 | "@stacksjs/newsletter": ["@stacksjs/newsletter@0.75.221", "", { "dependencies": { "@stacksjs/database": "0.75.221", "@stacksjs/email": "0.75.221", "@stacksjs/orm": "0.75.221", "@stacksjs/queue": "0.75.221" } }, "sha512-yK6x86CeAKqnefkSRddaziCfxKV4jnG4BXLhZdL47ObzMo4WN6RwDeNdNmnBQ4JJgWWcDSf4Sb9EYQJw12XBSw=="], | |
| 173 | 173 | |
| 174 | "@stacksjs/notifications": ["@stacksjs/notifications@0.75.219", "", { "dependencies": { "@stacksjs/chat": "0.75.219", "@stacksjs/cli": "0.75.219", "@stacksjs/config": "0.75.219", "@stacksjs/database": "0.75.219", "@stacksjs/datetime": "0.75.219", "@stacksjs/email": "0.75.219", "@stacksjs/push": "0.75.219", "@stacksjs/realtime": "0.75.219", "@stacksjs/sms": "0.75.219" } }, "sha512-t7IXZJxETJ5zsNt1iHIJn072el8cGGL7CkoN1pKkngIzJOORCBRDCgx32+7vCX1f9ST9Y8cA/ThcWQJETQ4nuQ=="], | |
| 174 | "@stacksjs/notifications": ["@stacksjs/notifications@0.75.221", "", { "dependencies": { "@stacksjs/chat": "0.75.221", "@stacksjs/cli": "0.75.221", "@stacksjs/config": "0.75.221", "@stacksjs/database": "0.75.221", "@stacksjs/datetime": "0.75.221", "@stacksjs/email": "0.75.221", "@stacksjs/push": "0.75.221", "@stacksjs/realtime": "0.75.221", "@stacksjs/sms": "0.75.221" } }, "sha512-oio5XfXY4WLCHpMCcfP5r4Pei2TFODKTD40KWS1wijs0I0jkPzzBfiaxA9sZtGlDWq9yAH0rrfMh6eZs/NteCw=="], | |
| 175 | 175 | |
| 176 | "@stacksjs/objects": ["@stacksjs/objects@0.75.219", "", { "dependencies": { "@stacksjs/utils": "0.75.219", "@stacksjs/validation": "0.75.219" } }, "sha512-e2UUJhBlxCepClOngb3O4fgF8J8MzhGPdqHs9JDZLTcRQd93H0psrfRyqXyQfxvsGnKgcMR8BOfjJvSzavGpMg=="], | |
| 176 | "@stacksjs/objects": ["@stacksjs/objects@0.75.221", "", { "dependencies": { "@stacksjs/utils": "0.75.221", "@stacksjs/validation": "0.75.221" } }, "sha512-O3brmL1MJLNAX+pUNWu0RpXTAYs2DEZKHIE7/+GLa+b9v4edUOTxY4m/bw8jvbWyebgrD2EEcLY+sVVToVOiIw=="], | |
| 177 | 177 | |
| 178 | "@stacksjs/orm": ["@stacksjs/orm@0.75.219", "", { "dependencies": { "@stacksjs/cache": "0.75.219", "@stacksjs/cli": "0.75.219", "@stacksjs/config": "0.75.219", "@stacksjs/database": "0.75.219", "@stacksjs/env": "0.75.219", "@stacksjs/error-handling": "0.75.219", "@stacksjs/events": "0.75.219", "@stacksjs/logging": "0.75.219", "@stacksjs/model-meta": "0.75.219", "@stacksjs/pagination": "0.75.219", "@stacksjs/path": "0.75.219", "@stacksjs/query-builder": "0.75.219", "@stacksjs/realtime": "0.75.219", "@stacksjs/router": "0.75.219", "@stacksjs/storage": "0.75.219", "@stacksjs/strings": "0.75.219", "@stacksjs/ts-validation": "^0.5.6", "@stacksjs/validation": "0.75.219", "bun-query-builder": "^0.3.8" }, "peerDependencies": { "@stacksjs/auth": "0.75.219", "@stacksjs/payments": "0.75.219", "@stacksjs/queue": "0.75.219", "@stacksjs/search-engine": "0.75.219", "stripe": "23.0.0" }, "optionalPeers": ["@stacksjs/auth", "@stacksjs/payments", "@stacksjs/queue", "@stacksjs/search-engine", "stripe"] }, "sha512-C3NGIiKqGXGfdKSqgTcrCH7ipf23tx3gMyLYoO5SDW6060qrdlSrRNwsmHB8rLNcMulwXb3yKULvqQ48GS+vng=="], | |
| 178 | "@stacksjs/orm": ["@stacksjs/orm@0.75.221", "", { "dependencies": { "@stacksjs/cache": "0.75.221", "@stacksjs/cli": "0.75.221", "@stacksjs/config": "0.75.221", "@stacksjs/database": "0.75.221", "@stacksjs/env": "0.75.221", "@stacksjs/error-handling": "0.75.221", "@stacksjs/events": "0.75.221", "@stacksjs/logging": "0.75.221", "@stacksjs/model-meta": "0.75.221", "@stacksjs/pagination": "0.75.221", "@stacksjs/path": "0.75.221", "@stacksjs/query-builder": "0.75.221", "@stacksjs/realtime": "0.75.221", "@stacksjs/router": "0.75.221", "@stacksjs/storage": "0.75.221", "@stacksjs/strings": "0.75.221", "@stacksjs/ts-validation": "^0.5.6", "@stacksjs/validation": "0.75.221", "bun-query-builder": "^0.3.8" }, "peerDependencies": { "@stacksjs/auth": "0.75.221", "@stacksjs/payments": "0.75.221", "@stacksjs/queue": "0.75.221", "@stacksjs/search-engine": "0.75.221", "stripe": "23.0.0" }, "optionalPeers": ["@stacksjs/auth", "@stacksjs/payments", "@stacksjs/queue", "@stacksjs/search-engine", "stripe"] }, "sha512-CpIMYShnDKRzO0HOzb05ehIN3c6dInbByOEwZRoRjpoZZrh9+Q9dl6cD2jFVIi9WZi7OEYak5ZcEMRh4a6nScA=="], | |
| 179 | 179 | |
| 180 | "@stacksjs/pagination": ["@stacksjs/pagination@0.75.219", "", {}, "sha512-lPcNKULqF1h2/zPjgh9AivwxbNsasZnwS4L4oXy7w9zWJAUpDqmw212gUqlZrIWcS/mDYP/mOpEuZEY2sOf1QQ=="], | |
| 180 | "@stacksjs/pagination": ["@stacksjs/pagination@0.75.221", "", {}, "sha512-y1UkhVhyp2rqpW/hxvFho7zGCHgj1BBEKIb7LRUCYE8p/EwzuHPXgn08iQ+7DX79LLDDLIX/vihfOY7NnJ3PMw=="], | |
| 181 | 181 | |
| 182 | "@stacksjs/path": ["@stacksjs/path@0.75.219", "", {}, "sha512-5GX6flfE+eSlmEb9arrlRFibVySE8R4wOCvhaqGiSbmYoP7VkGTz4bxvZLkqR+AdKLvVDcDdGa2z4dITFBewxA=="], | |
| 182 | "@stacksjs/path": ["@stacksjs/path@0.75.221", "", {}, "sha512-E7QpuOf3yOzPyInvcCNqwTm/WYww2xSGqjY6jQu23OwWxOQM/NMeChseXmr4Lm2QVbfN+g0QGq2+xbh7B7oq2Q=="], | |
| 183 | 183 | |
| 184 | "@stacksjs/payments": ["@stacksjs/payments@0.75.219", "", { "dependencies": { "@stacksjs/config": "0.75.219", "@stacksjs/database": "0.75.219", "@stacksjs/error-handling": "0.75.219", "@stacksjs/logging": "0.75.219", "@stacksjs/orm": "0.75.219", "@stacksjs/types": "0.75.219" }, "peerDependencies": { "@stripe/stripe-js": "^10.0.0", "stripe": "23.0.0" }, "optionalPeers": ["@stripe/stripe-js", "stripe"] }, "sha512-shSMvujyPYuUleDPkoGynobCJINW/yZDz5XKO++5MMvOKoM6I99Ca7RRjcwAxUkmqKydOUvm5YGFgpETdBk6AQ=="], | |
| 184 | "@stacksjs/payments": ["@stacksjs/payments@0.75.221", "", { "dependencies": { "@stacksjs/config": "0.75.221", "@stacksjs/database": "0.75.221", "@stacksjs/error-handling": "0.75.221", "@stacksjs/logging": "0.75.221", "@stacksjs/orm": "0.75.221", "@stacksjs/types": "0.75.221" }, "peerDependencies": { "@stripe/stripe-js": "^10.0.0", "stripe": "23.0.0" }, "optionalPeers": ["@stripe/stripe-js", "stripe"] }, "sha512-LiTJRssFY5RPIGwsa/LkCcX+m6DPv876D1O3hZYn8TCecIIGkjHgeBuW2a9vX6MPT99Kiev1dwVbI0bhtDyuHA=="], | |
| 185 | 185 | |
| 186 | "@stacksjs/push": ["@stacksjs/push@0.75.219", "", { "dependencies": { "@stacksjs/cli": "0.75.219", "@stacksjs/config": "0.75.219" } }, "sha512-uTSdndHnR7Rkc5d5hZmU9bXT0ReDgJ4K9FEZ1VY8mB3qHifTsxXoDYxQNbYsvY3DRksXtTh3DzsoXn+bb7VYOg=="], | |
| 186 | "@stacksjs/push": ["@stacksjs/push@0.75.221", "", { "dependencies": { "@stacksjs/cli": "0.75.221", "@stacksjs/config": "0.75.221" } }, "sha512-wPRfmENu1X0FGlV0CH+stZHE8Fq+sbVs9tJnm7MkENlALmfCNAXVxmJmhtZhqoWjjlBLzFoF/DNKgWqoOkx/fw=="], | |
| 187 | 187 | |
| 188 | "@stacksjs/query-builder": ["@stacksjs/query-builder@0.75.219", "", { "dependencies": { "bun-query-builder": "^0.3.13" } }, "sha512-Pf/fropB6rURr2+FCmx3/zMSJc35ata+OmETLjEiJqjkJgh9ib4yJkR4bsgM3JWxb7R50b1WqFwHNDHY4rGeKA=="], | |
| 188 | "@stacksjs/query-builder": ["@stacksjs/query-builder@0.75.221", "", { "dependencies": { "bun-query-builder": "^0.3.13" } }, "sha512-DLOwoG8psEtTTleYe7DMVCcqvq20rDUVSR8RAFz3D/Ksclvfi8NytOV2idlPRAtqcBzW/GDRpasNAkztoh3Cog=="], | |
| 189 | 189 | |
| 190 | "@stacksjs/queue": ["@stacksjs/queue@0.75.219", "", { "dependencies": { "@stacksjs/action-runner": "0.75.219", "@stacksjs/bun-queue": "^0.1.8", "@stacksjs/cache": "0.75.219", "@stacksjs/config": "0.75.219", "@stacksjs/cron": "0.75.219", "@stacksjs/database": "0.75.219", "@stacksjs/env": "0.75.219", "@stacksjs/error-handling": "0.75.219", "@stacksjs/logging": "0.75.219", "@stacksjs/path": "0.75.219" }, "peerDependencies": { "@stacksjs/email": "0.75.219", "@stacksjs/router": "0.75.219" }, "optionalPeers": ["@stacksjs/email", "@stacksjs/router"] }, "sha512-+2E+G/IzOajV61bP29Sfohz5xlYOzBazsJdQoMs4xObpksX2UjJEDPepsqH7X9IQAhaLsskH8VNEDKnLbFvdLw=="], | |
| 190 | "@stacksjs/queue": ["@stacksjs/queue@0.75.221", "", { "dependencies": { "@stacksjs/action-runner": "0.75.221", "@stacksjs/bun-queue": "^0.1.8", "@stacksjs/cache": "0.75.221", "@stacksjs/config": "0.75.221", "@stacksjs/cron": "0.75.221", "@stacksjs/database": "0.75.221", "@stacksjs/env": "0.75.221", "@stacksjs/error-handling": "0.75.221", "@stacksjs/logging": "0.75.221", "@stacksjs/path": "0.75.221" }, "peerDependencies": { "@stacksjs/email": "0.75.221", "@stacksjs/router": "0.75.221" }, "optionalPeers": ["@stacksjs/email", "@stacksjs/router"] }, "sha512-Si73rtRX92vgCHL5WeBclehKxMOtoCtJm8GJU1RhhyJLZe9nEmdmwmWnXNxjePRHp5vUdqxszEdviVFKa/teVA=="], | |
| 191 | 191 | |
| 192 | "@stacksjs/realtime": ["@stacksjs/realtime@0.75.219", "", { "dependencies": { "@stacksjs/logging": "0.75.219", "@stacksjs/path": "0.75.219", "ts-broadcasting": "^0.0.11" } }, "sha512-QD4wZNU9kteFMqT4X1a6a3mP7js7kpsnhw/gnQIBmkiXzf0mrMkqkudTVzAdGrqQmGidv+uJzkqp4geUm6wUQQ=="], | |
| 192 | "@stacksjs/realtime": ["@stacksjs/realtime@0.75.221", "", { "dependencies": { "@stacksjs/logging": "0.75.221", "@stacksjs/path": "0.75.221", "ts-broadcasting": "^0.0.11" } }, "sha512-S50ERQuZuPh68TYmFS0FR8t0a0MtuQjMB9Dq/Z7IpJg6sBy5ocxHwYI7Bfq5dqGwkOT2oiHaAHXjbuCA+fItww=="], | |
| 193 | 193 | |
| 194 | "@stacksjs/registry": ["@stacksjs/registry@0.75.219", "", { "dependencies": { "@stacksjs/types": "0.75.219" } }, "sha512-rCODm8mYfk6C7Af3QmsJ/TXlNCq7/mwNhSemlLU97kTex6Mojhhhl1IZGh3HWCnlcsxFLjqiRnRtGmsaVkmzYQ=="], | |
| 194 | "@stacksjs/registry": ["@stacksjs/registry@0.75.221", "", { "dependencies": { "@stacksjs/types": "0.75.221" } }, "sha512-b8DEKPiqcuJMhWKjev1VXU43ansUV9pWjTMh/1yM3UtAJ+GBqn+IUGicYcevbzbLIFoYY0IvJD/10UxGjcWnUQ=="], | |
| 195 | 195 | |
| 196 | "@stacksjs/repl": ["@stacksjs/repl@0.75.219", "", { "dependencies": { "@stacksjs/tinker": "0.75.219" } }, "sha512-KWUETh2wQoa0MlEe3LS7NFMtFGFMb09gGR2xqVNWlMou/qVXkRfE77864D6vq2occKRFisDxOCdavMaxJxqdzg=="], | |
| 196 | "@stacksjs/repl": ["@stacksjs/repl@0.75.221", "", { "dependencies": { "@stacksjs/tinker": "0.75.221" } }, "sha512-SYf24pkA2zYsGvr21zqkI5BUUMK8NsywZdCL60U2cI68LsSYTxgBnevSl2znqAiW6aN9kPL+uSEzz3OSyH7qeQ=="], | |
| 197 | 197 | |
| 198 | "@stacksjs/router": ["@stacksjs/router@0.75.219", "", { "dependencies": { "@stacksjs/bun-router": "^0.1.24", "@stacksjs/cache": "0.75.219", "@stacksjs/collections": "0.75.219", "@stacksjs/config": "0.75.219", "@stacksjs/error-handling": "0.75.219", "@stacksjs/logging": "0.75.219", "@stacksjs/pagination": "0.75.219", "@stacksjs/path": "0.75.219", "@stacksjs/security": "0.75.219", "@stacksjs/storage": "0.75.219", "ts-rate-limiter": "^0.4.10" }, "peerDependencies": { "@stacksjs/api": "0.75.219", "@stacksjs/auth": "0.75.219", "@stacksjs/database": "0.75.219", "@stacksjs/validation": "0.75.219" }, "optionalPeers": ["@stacksjs/api", "@stacksjs/auth", "@stacksjs/database", "@stacksjs/validation"] }, "sha512-9r0V7S6WgFUnOpFdDGKDhtIHtRC6qqaOKF8ZnaBMFDG92OInzXMe2jrbHp6W5opC7Zi4/ezPpMCD+tROvEVC6g=="], | |
| 198 | "@stacksjs/router": ["@stacksjs/router@0.75.221", "", { "dependencies": { "@stacksjs/bun-router": "^0.1.24", "@stacksjs/cache": "0.75.221", "@stacksjs/collections": "0.75.221", "@stacksjs/config": "0.75.221", "@stacksjs/error-handling": "0.75.221", "@stacksjs/logging": "0.75.221", "@stacksjs/pagination": "0.75.221", "@stacksjs/path": "0.75.221", "@stacksjs/security": "0.75.221", "@stacksjs/storage": "0.75.221", "ts-rate-limiter": "^0.4.10" }, "peerDependencies": { "@stacksjs/api": "0.75.221", "@stacksjs/auth": "0.75.221", "@stacksjs/database": "0.75.221", "@stacksjs/validation": "0.75.221" }, "optionalPeers": ["@stacksjs/api", "@stacksjs/auth", "@stacksjs/database", "@stacksjs/validation"] }, "sha512-q+twwIaPsDmr+QdqgNwuQkD/0SC0tL73yGKjA0l/wRcOM61O7IyZAkBNrp69khicIrv66/CnoRhUmpVjHyv48g=="], | |
| 199 | 199 | |
| 200 | 200 | "@stacksjs/rpx": ["@stacksjs/rpx@0.11.54", "", { "dependencies": { "@stacksjs/clapp": "^0.2.10", "@stacksjs/tlsx": "^0.13.19", "ts-images": "^0.2.17" }, "bin": { "rpx": "./dist/bin/cli.js", "reverse-proxy": "./dist/bin/cli.js" } }, "sha512-3njfy0BYCpof1rkuY4slGmHRggLWlJdrnUJMNoou6kfBqXNvm8ANqXgiL/bIlQeIWwidqoo1IOjgPcB/EozNog=="], |
| 201 | 201 | |
| 202 | "@stacksjs/sanitizer": ["@stacksjs/sanitizer@0.2.563", "", {}, "sha512-JkhUgIuiyTmjegQLKe+LEvdbgY+ACrnO0uurj3DNU95MhTtLLB6L/XmJCVHw68PUMbeZVVUw/hBXx4/SrUNg9g=="], | |
| 202 | "@stacksjs/sanitizer": ["@stacksjs/sanitizer@0.2.573", "", {}, "sha512-kcDa3Anv1MwJk4DkUeUromb1H2aPFAD02ELBXSlACjHVSvgQFj0gzmgl2f6B3f2/kB0hcEbfNHj7LiCe9xY+hA=="], | |
| 203 | 203 | |
| 204 | "@stacksjs/scheduler": ["@stacksjs/scheduler@0.75.219", "", { "dependencies": { "@stacksjs/action-runner": "0.75.219", "@stacksjs/cli": "0.75.219", "@stacksjs/cron": "0.75.219", "@stacksjs/database": "0.75.219", "@stacksjs/env": "0.75.219", "@stacksjs/error-handling": "0.75.219", "@stacksjs/logging": "0.75.219", "@stacksjs/notifications": "0.75.219", "@stacksjs/path": "0.75.219", "@stacksjs/queue": "0.75.219", "@stacksjs/storage": "0.75.219", "@stacksjs/types": "0.75.219" } }, "sha512-ourjJ5S/zcW/5QWvbLUuS7rw0KBbp6bpMM4sJBxeZ+ad6Cvw50v/y3iLn35WCebQAlNgwPnS3PRg1KPDtN6qLw=="], | |
| 204 | "@stacksjs/scheduler": ["@stacksjs/scheduler@0.75.221", "", { "dependencies": { "@stacksjs/action-runner": "0.75.221", "@stacksjs/cli": "0.75.221", "@stacksjs/cron": "0.75.221", "@stacksjs/database": "0.75.221", "@stacksjs/env": "0.75.221", "@stacksjs/error-handling": "0.75.221", "@stacksjs/logging": "0.75.221", "@stacksjs/notifications": "0.75.221", "@stacksjs/path": "0.75.221", "@stacksjs/queue": "0.75.221", "@stacksjs/storage": "0.75.221", "@stacksjs/types": "0.75.221" } }, "sha512-j5wRcyeN9bKBd70mP1c6W+8Xh2IS8n2TMbTl85Z1iPt2jrlrHUuY+EwcJwJYB0IJBiKnnp8j30bJm2FHlbJtBA=="], | |
| 205 | 205 | |
| 206 | "@stacksjs/search-engine": ["@stacksjs/search-engine@0.75.219", "", { "dependencies": { "@stacksjs/config": "0.75.219", "@stacksjs/error-handling": "0.75.219", "@stacksjs/logging": "0.75.219", "@stacksjs/orm": "0.75.219", "@stacksjs/path": "0.75.219", "@stacksjs/storage": "0.75.219", "@stacksjs/strings": "0.75.219" }, "peerDependencies": { "meilisearch": "^0.62.0" }, "optionalPeers": ["meilisearch"] }, "sha512-97nIsTppDccdSK4JXN6MukSCaYs3OhSc36gzTTLrNO+yMdBi8RoK8S/7oM6y0CpqcgLdy6NLbjACuaIqktX/9Q=="], | |
| 206 | "@stacksjs/search-engine": ["@stacksjs/search-engine@0.75.221", "", { "dependencies": { "@stacksjs/config": "0.75.221", "@stacksjs/error-handling": "0.75.221", "@stacksjs/logging": "0.75.221", "@stacksjs/orm": "0.75.221", "@stacksjs/path": "0.75.221", "@stacksjs/storage": "0.75.221", "@stacksjs/strings": "0.75.221" }, "peerDependencies": { "meilisearch": "^0.62.0" }, "optionalPeers": ["meilisearch"] }, "sha512-yK2/jnOuVq/I8lCgaP7psJNHxpp4gE85GrR3sT5z4Boe+Yg1z34mRG87IhTEncDuYLp/ta5QMwgAsBBE8W0s3Q=="], | |
| 207 | 207 | |
| 208 | "@stacksjs/security": ["@stacksjs/security@0.75.219", "", { "dependencies": { "@stacksjs/config": "0.75.219", "ts-security-crypto": "0.0.2" } }, "sha512-V/bw7eFLVBKJij4Ue69+NEH8kDbhCXbUCNbyaKCvRBRlGnuv/HDecM9zwNwycqvR1tSohvwSuEO8sZX20d1Khw=="], | |
| 208 | "@stacksjs/security": ["@stacksjs/security@0.75.221", "", { "dependencies": { "@stacksjs/config": "0.75.221", "ts-security-crypto": "0.0.2" } }, "sha512-v0KElp70zQj8b401jHIaqOkFZ3bNH7FcNOTFwOIOkwYK3AEUh69R29xMptna6tGhQ1rmLpSPo+2LVECo+KTkiQ=="], | |
| 209 | 209 | |
| 210 | "@stacksjs/server": ["@stacksjs/server@0.75.219", "", { "dependencies": { "@stacksjs/auth": "0.75.219", "@stacksjs/bun-router": "^0.1.24", "@stacksjs/config": "0.75.219", "@stacksjs/events": "0.75.219", "@stacksjs/i18n": "0.75.219", "@stacksjs/logging": "0.75.219", "@stacksjs/orm": "0.75.219", "@stacksjs/path": "0.75.219", "@stacksjs/payments": "0.75.219", "@stacksjs/router": "0.75.219", "@stacksjs/storage": "0.75.219", "bun-plugin-auto-imports": "^0.4.4" } }, "sha512-EMPtc9uQtnSX+sZX1Rdl82p+yzIdi0I+VHXYdKemaDIz85JVS+11Th9h/v2iy8EwKS14KoGsgzesy9gFuvidQQ=="], | |
| 210 | "@stacksjs/server": ["@stacksjs/server@0.75.221", "", { "dependencies": { "@stacksjs/auth": "0.75.221", "@stacksjs/bun-router": "^0.1.24", "@stacksjs/config": "0.75.221", "@stacksjs/events": "0.75.221", "@stacksjs/i18n": "0.75.221", "@stacksjs/logging": "0.75.221", "@stacksjs/orm": "0.75.221", "@stacksjs/path": "0.75.221", "@stacksjs/payments": "0.75.221", "@stacksjs/router": "0.75.221", "@stacksjs/storage": "0.75.221", "bun-plugin-auto-imports": "^0.4.4" } }, "sha512-UZY4n7/yVNOphIzIRqcnufiSIdGka2aSfAD5+JKhFmRju2JNXySZczhAhq07ZAxu/VLb4bQ9ZtA0nQxoKC+cqw=="], | |
| 211 | 211 | |
| 212 | "@stacksjs/shell": ["@stacksjs/shell@0.75.219", "", {}, "sha512-g9YdInB+fz89zz8sKuaEP9/2NlrpbLc2D6DLphw2wp70qWDL9PnUDyOe9p+TYnUeO6mfr+wNIVHqOhderLv7qg=="], | |
| 212 | "@stacksjs/shell": ["@stacksjs/shell@0.75.221", "", {}, "sha512-10np3JLzeDP2ioC8SryL0zmZ6TvZCh1Gb+fu8sW4XFle++NpOZ7ex1GlPbN1Ob1mEyLTsj1Zfl5IxacNdRH9rw=="], | |
| 213 | 213 | |
| 214 | "@stacksjs/sites": ["@stacksjs/sites@0.75.219", "", { "dependencies": { "@stacksjs/auth": "0.75.219", "@stacksjs/config": "0.75.219", "@stacksjs/database": "0.75.219", "@stacksjs/router": "0.75.219" }, "peerDependencies": { "@stacksjs/cms": "0.75.219" }, "optionalPeers": ["@stacksjs/cms"] }, "sha512-9uxfougtwuMYCiD1CItbDbPvcI1hrUo0NOHByBD7yR5kudMvJjHCo/SrWus7iK8k92rU+T0w4cP8CYE5imYeew=="], | |
| 214 | "@stacksjs/sites": ["@stacksjs/sites@0.75.221", "", { "dependencies": { "@stacksjs/auth": "0.75.221", "@stacksjs/config": "0.75.221", "@stacksjs/database": "0.75.221", "@stacksjs/router": "0.75.221" }, "peerDependencies": { "@stacksjs/cms": "0.75.221" }, "optionalPeers": ["@stacksjs/cms"] }, "sha512-Al4ueNVSE3LEeFjP+dpVJ99+8bXhbK7/lrDoEgSEO/x5MTjBBotTB4DcrVPJNE6sqpuxUO9CaTEBDx9zKhKOaw=="], | |
| 215 | 215 | |
| 216 | "@stacksjs/skills": ["@stacksjs/skills@0.75.219", "", { "dependencies": { "@stacksjs/path": "0.75.219", "@stacksjs/storage": "0.75.219" } }, "sha512-zrSw4QoBiiLm08l8dFhZES498j9dDPMrblRvdSOsmpvyygVzBU6/SUX26JgP+mh8pkxtkMpORngv/w3NJwNIIQ=="], | |
| 216 | "@stacksjs/skills": ["@stacksjs/skills@0.75.221", "", { "dependencies": { "@stacksjs/path": "0.75.221", "@stacksjs/storage": "0.75.221" } }, "sha512-jcPWbY45OMnUqKXNaOMl5TKJnx/nKrXluSrVbaWjSn90qhLOdQdD4BfRWW1niMQnvy9szPMTVZqSy+i9gxTmOw=="], | |
| 217 | 217 | |
| 218 | "@stacksjs/slug": ["@stacksjs/slug@0.75.219", "", { "dependencies": { "@stacksjs/database": "0.75.219", "ts-slug": "^0.1.0" } }, "sha512-AHBN0M4Cgs2CFNAA01eZiUMpdEHGqm5UqIbqsO5xAhpXtzSkmpx2P0T+5eywZC04C4fnpYl2GJ4yTziQU3kkNQ=="], | |
| 218 | "@stacksjs/slug": ["@stacksjs/slug@0.75.221", "", { "dependencies": { "@stacksjs/database": "0.75.221", "ts-slug": "^0.1.0" } }, "sha512-oedPoXEUOHakcPhtGYP8vOR4oFqUAxu5PFSftt7vPhjZyU3bb27N04E58tpiONFsPVhxtJnB+KOk25sE9MJqJQ=="], | |
| 219 | 219 | |
| 220 | "@stacksjs/sms": ["@stacksjs/sms@0.75.219", "", { "dependencies": { "@stacksjs/database": "0.75.219" } }, "sha512-hCaOxzUy06DK7TFb417g2EPrEC3p2J0KKNnavJxUyfNy04nItTexW5IvnucIMtilz+xE/GGNkNSo87BfWATX3A=="], | |
| 220 | "@stacksjs/sms": ["@stacksjs/sms@0.75.221", "", { "dependencies": { "@stacksjs/database": "0.75.221" } }, "sha512-qWkuasYoQmE/EUMk9OriZs9kLkbqZKlOKz6wYf/OxbLsJOX/Qqf4XM2wes1QqwW1A4KYl5GJr0tj5SY3Lpzm5w=="], | |
| 221 | 221 | |
| 222 | "@stacksjs/socials": ["@stacksjs/socials@0.75.219", "", { "dependencies": { "@stacksjs/api": "0.75.219", "@stacksjs/composables": "0.75.219", "@stacksjs/config": "0.75.219" } }, "sha512-EWM17eGeHlE3c/vJsfGPFWLf6IoZal5Nu651QK6vdVg4unXKlY3dNvAW6Cc2PqfBtU55XplxyQ14ZWH6S6qvdQ=="], | |
| 222 | "@stacksjs/socials": ["@stacksjs/socials@0.75.221", "", { "dependencies": { "@stacksjs/api": "0.75.221", "@stacksjs/composables": "0.75.221", "@stacksjs/config": "0.75.221" } }, "sha512-6SVr7QOa0XchLmaQ2KvBBKXSwl+P6Oxo4sAfhJRdRJI4ibj5a5rXi8IiYSMdTlHR5h99jiQ9H//5GOisA5tl8w=="], | |
| 223 | 223 | |
| 224 | "@stacksjs/storage": ["@stacksjs/storage@0.75.219", "", { "dependencies": { "@stacksjs/arrays": "0.75.219", "@stacksjs/cli": "0.75.219", "@stacksjs/config": "0.75.219", "@stacksjs/env": "0.75.219", "@stacksjs/error-handling": "0.75.219", "@stacksjs/logging": "0.75.219", "@stacksjs/path": "0.75.219", "@stacksjs/strings": "0.75.219", "@stacksjs/ts-cloud": "^0.16.58", "@stacksjs/ts-xml": "^0.1.0", "ts-images": "^0.2.23" } }, "sha512-o+Asg6+YElTa1UlcCWycWv/j98Enp8TWvJUZ5OKUI+Id3PlsmmCOgzLnjFdt8ImOE6tMtqKhG4dq0mcIArkTBA=="], | |
| 224 | "@stacksjs/storage": ["@stacksjs/storage@0.75.221", "", { "dependencies": { "@stacksjs/arrays": "0.75.221", "@stacksjs/cli": "0.75.221", "@stacksjs/config": "0.75.221", "@stacksjs/env": "0.75.221", "@stacksjs/error-handling": "0.75.221", "@stacksjs/logging": "0.75.221", "@stacksjs/path": "0.75.221", "@stacksjs/strings": "0.75.221", "@stacksjs/ts-cloud": "^0.16.58", "@stacksjs/ts-xml": "^0.1.0", "ts-images": "^0.2.23" } }, "sha512-d7AAC2QqHCIVAikmrDvjYY0bDQPw5zgQO3F8X90Mml4h9x/1ccRB67tNSYYyMWcA82sfkixxBRRYiItpwElibg=="], | |
| 225 | 225 | |
| 226 | "@stacksjs/strings": ["@stacksjs/strings@0.75.219", "", {}, "sha512-88rZr9v1bONXcmfcteX1L/pK98LeN8IDVVaIJA5rjp4sd4xhKmB7rnfY4Bbz+GDcF9i0Q6FC289iDiLPUjaUWQ=="], | |
| 226 | "@stacksjs/strings": ["@stacksjs/strings@0.75.221", "", {}, "sha512-61MfF3GTIYS8+wa9sMH85Ko6AaAFXlf6muwi1Koe1NnHlzBY2ZyHlUxwULPqHM2tmYZOk1/hzh9Ep7FXTVfDiQ=="], | |
| 227 | 227 | |
| 228 | "@stacksjs/stx": ["@stacksjs/stx@0.2.563", "", { "dependencies": { "@stacksjs/clapp": "^0.2.16", "@stacksjs/desktop": "0.2.563", "@stacksjs/sanitizer": "0.2.563", "@stacksjs/ts-css": "^0.3.11", "@stacksjs/ts-i18n": "^0.1.11", "bun-plugin-stx": "0.2.563", "bunfig": "^0.15.19", "stx-router": "0.2.563", "ts-broadcasting": "^0.0.11", "ts-images": "^0.2.22", "ts-syntax-highlighter": "^0.2.1" }, "bin": { "stx": "./dist/cli.js" } }, "sha512-WTNOmbdnZ8ZfJp37fGGKc9C4R6xpDFAwauPeEdY/JX9s8A0BZX5cPBsB+4iNeLeOyQ7iFP5fz8Po/4Qmm3cNng=="], | |
| 228 | "@stacksjs/stx": ["@stacksjs/stx@0.2.573", "", { "dependencies": { "@stacksjs/clapp": "^0.2.16", "@stacksjs/desktop": "0.2.573", "@stacksjs/sanitizer": "0.2.573", "@stacksjs/ts-css": "^0.3.11", "@stacksjs/ts-i18n": "^0.1.11", "bun-plugin-stx": "0.2.573", "bunfig": "^0.15.21", "stx-router": "0.2.573", "ts-broadcasting": "^0.0.11", "ts-images": "^0.2.22", "ts-syntax-highlighter": "^0.2.1" }, "bin": { "stx": "./dist/cli.js" } }, "sha512-G7gjDITc2lzGNnvXnPmecPOJARUFKQWLmzMQtN3bjteF+uX7gvPdVE3Ns791WoketdM2xMrGSwoUS7epNklY7A=="], | |
| 229 | 229 | |
| 230 | "@stacksjs/testing": ["@stacksjs/testing@0.75.219", "", { "dependencies": { "@stacksjs/auth": "0.75.219", "@stacksjs/cache": "0.75.219", "@stacksjs/config": "0.75.219", "@stacksjs/database": "0.75.219", "@stacksjs/email": "0.75.219", "@stacksjs/events": "0.75.219", "@stacksjs/path": "0.75.219", "@stacksjs/router": "0.75.219", "@stacksjs/storage": "0.75.219", "@stacksjs/strings": "0.75.219", "@stacksjs/ts-cloud": "^0.16.58", "very-happy-dom": "^0.3.5" } }, "sha512-OclrQD3AAOTbU5NHbM1Ls3COOMYAqLLg8+e2jS63gQaXmYfqbjBhLSMt83F+6Xeihmup3puin9XsGGA+fe6Gzw=="], | |
| 230 | "@stacksjs/testing": ["@stacksjs/testing@0.75.221", "", { "dependencies": { "@stacksjs/auth": "0.75.221", "@stacksjs/cache": "0.75.221", "@stacksjs/config": "0.75.221", "@stacksjs/database": "0.75.221", "@stacksjs/email": "0.75.221", "@stacksjs/events": "0.75.221", "@stacksjs/path": "0.75.221", "@stacksjs/router": "0.75.221", "@stacksjs/storage": "0.75.221", "@stacksjs/strings": "0.75.221", "@stacksjs/ts-cloud": "^0.16.58", "very-happy-dom": "^0.3.5" } }, "sha512-ijtsuuxywbEZDlyIzr5MR1WXkTNHyl2TLnzWy+xK3iPCYe8tIGr6B6fZAPjsLfqp8Y61OgSnQG02Qmet6FUFcg=="], | |
| 231 | 231 | |
| 232 | "@stacksjs/tinker": ["@stacksjs/tinker@0.75.219", "", {}, "sha512-z4KZKQMpMSmyKtJvEnf4Cbp8Jr+bYWYxCSe+6q74BGlyrj8e0FSZf2FIO90HssW5hzNj52wz8Ox7QH2OxrW0CQ=="], | |
| 232 | "@stacksjs/tinker": ["@stacksjs/tinker@0.75.221", "", {}, "sha512-4W0CL5LTfw+/OE0yiymXFUv4UO6cqjtysGRzzc4OeX0Y/F4iDxfQFviSporoNyFsSP3GpDIbaVQz7MWc9IlnrQ=="], | |
| 233 | 233 | |
| 234 | 234 | "@stacksjs/tlsx": ["@stacksjs/tlsx@0.13.19", "", { "dependencies": { "@stacksjs/clapp": "^0.2.0" }, "bin": { "tlsx": "./dist/bin/cli.js" } }, "sha512-bgVYWBolcbbTSDk/quZiYWfz3qBZiHl1fp9pqTYaeDHXIHxylRfxDFB3bz7mnEmIYSFq9AXilRNkV401uUSGSw=="], |
| 235 | 235 | |
| @@ -265,19 +265,19 @@ | ||
| 265 | 265 | |
| 266 | 266 | "@stacksjs/ts-xml": ["@stacksjs/ts-xml@0.1.2", "", {}, "sha512-G20EuKCmpwWwnXfFlX0kxCXnJ6ocTgBl8lcHkOzmomC8L/Ld+UKeNLlvOQf9ArnS7/SHUQ2+fZpM0t6lhwoyPw=="], |
| 267 | 267 | |
| 268 | "@stacksjs/tunnel": ["@stacksjs/tunnel@0.75.219", "", { "dependencies": { "localtunnels": "^0.2.13" } }, "sha512-7y/AU7ozW16xskrFbncO8ZX94lln0wQgZiuomVj4/MwiaErUBEEX8A0fbfxgx+FoECN8WAFoiinMb7FaUZeqwQ=="], | |
| 268 | "@stacksjs/tunnel": ["@stacksjs/tunnel@0.75.221", "", { "dependencies": { "localtunnels": "^0.2.13" } }, "sha512-fNSq23ZcTqkn4N7R5NMrhYsRqFq9tNbJxMYiCrw4vsNwpjsMeuxTaBYDfyUuqan/rnf3dwSnCKvbrUr6u0po6w=="], | |
| 269 | 269 | |
| 270 | "@stacksjs/types": ["@stacksjs/types@0.75.219", "", { "dependencies": { "@stacksjs/bunpress": "^0.2.15", "@stacksjs/clapp": "^0.2.12", "@stacksjs/events": "0.75.219", "@stacksjs/strings": "0.75.219", "@stacksjs/stx": "^0.2.534", "@stacksjs/ts-validation": "^0.5.6", "bun-query-builder": "^0.3.8" }, "peerDependencies": { "meilisearch": "^0.62.0" }, "optionalPeers": ["meilisearch"] }, "sha512-VEP7zK92AsHpZKsCUmQ7XQCgbVX2Gsa3LMr/6+KCt0WY/XUxg3bC00r9yP+08eJafcR5GigMg2DXpVI1GNF1ww=="], | |
| 270 | "@stacksjs/types": ["@stacksjs/types@0.75.221", "", { "dependencies": { "@stacksjs/bunpress": "^0.2.15", "@stacksjs/clapp": "^0.2.12", "@stacksjs/events": "0.75.221", "@stacksjs/strings": "0.75.221", "@stacksjs/stx": "^0.2.534", "@stacksjs/ts-validation": "^0.5.6", "bun-query-builder": "^0.3.8" }, "peerDependencies": { "meilisearch": "^0.62.0" }, "optionalPeers": ["meilisearch"] }, "sha512-hqa/x50slPiu+KEP5XXer/j73QwAMeln6ApVmeEiMZo036air8ZxiKDH2wW+UCNNILoabTIBdLt/IogxiU1aYQ=="], | |
| 271 | 271 | |
| 272 | "@stacksjs/ui": ["@stacksjs/ui@0.75.219", "", { "dependencies": { "@stacksjs/components": "^0.2.534", "@stacksjs/config": "0.75.219", "@stacksjs/ts-css": "^0.3.6" } }, "sha512-nw8z3QzxffAIkYtTKoABlS2MdkDkmZ5oKQlF3EshN8w9GarfaGXNO88deBE2h/EyuWRsCVcVoxRv3HnxzlAIjQ=="], | |
| 272 | "@stacksjs/ui": ["@stacksjs/ui@0.75.221", "", { "dependencies": { "@stacksjs/components": "^0.2.534", "@stacksjs/config": "0.75.221", "@stacksjs/ts-css": "^0.3.6" } }, "sha512-jHlD6QrLtAMkPje0Lb1jF9M9CqzX3lerMLzSgXMkDIb+6Lz1YrrkY/Dabck3zlFA20WFe0ya/u8Vv31hlmidyA=="], | |
| 273 | 273 | |
| 274 | "@stacksjs/utils": ["@stacksjs/utils@0.75.219", "", { "dependencies": { "@stacksjs/browser": "0.75.219", "@stacksjs/cli": "0.75.219", "@stacksjs/config": "0.75.219", "@stacksjs/enums": "0.75.219", "@stacksjs/error-handling": "0.75.219", "@stacksjs/logging": "0.75.219", "@stacksjs/path": "0.75.219", "@stacksjs/storage": "0.75.219", "@stacksjs/strings": "0.75.219", "@stacksjs/types": "0.75.219" } }, "sha512-5ltSZ4sdzBBwZg9z7OmpkdyAlHspkCuj4FmoaSYECEqvvLs8G5x4/Q+k/+vyIheEDYb/3AMqilm7MqNRJskq/A=="], | |
| 274 | "@stacksjs/utils": ["@stacksjs/utils@0.75.221", "", { "dependencies": { "@stacksjs/browser": "0.75.221", "@stacksjs/cli": "0.75.221", "@stacksjs/config": "0.75.221", "@stacksjs/enums": "0.75.221", "@stacksjs/error-handling": "0.75.221", "@stacksjs/logging": "0.75.221", "@stacksjs/path": "0.75.221", "@stacksjs/storage": "0.75.221", "@stacksjs/strings": "0.75.221", "@stacksjs/types": "0.75.221" } }, "sha512-15YFKedRWkhLcExnM3nF8fDM//ZGrQwXhho3hBUoznUTg6RZeklCrICe/ZiXZu4xi/r/g1/T8BLphLiTTviY9A=="], | |
| 275 | 275 | |
| 276 | "@stacksjs/validation": ["@stacksjs/validation@0.75.219", "", { "dependencies": { "@stacksjs/error-handling": "0.75.219", "@stacksjs/path": "0.75.219", "@stacksjs/strings": "0.75.219", "@stacksjs/ts-validation": "^0.5.6", "@stacksjs/types": "0.75.219", "@stacksjs/utils": "0.75.219" }, "peerDependencies": { "@stacksjs/database": "0.75.219" }, "optionalPeers": ["@stacksjs/database"] }, "sha512-XHbbj82dzPAKD2AVLTT+VkPgx2a3wbvVqLehVzlTZO9+WW0nG1hh8wwrZAV970xDfXlw1A2G1GnnexJIy8iYcA=="], | |
| 276 | "@stacksjs/validation": ["@stacksjs/validation@0.75.221", "", { "dependencies": { "@stacksjs/error-handling": "0.75.221", "@stacksjs/path": "0.75.221", "@stacksjs/strings": "0.75.221", "@stacksjs/ts-validation": "^0.5.6", "@stacksjs/types": "0.75.221", "@stacksjs/utils": "0.75.221" }, "peerDependencies": { "@stacksjs/database": "0.75.221" }, "optionalPeers": ["@stacksjs/database"] }, "sha512-fFjecg396NtQXqRf6HmQx82uDEY/PfIrJvRXJSu4b1HvyFLTg+1XRsm7kOMS6hf6iDHYEhU2P8xIfOAh1Ez6yw=="], | |
| 277 | 277 | |
| 278 | "@stacksjs/video": ["@stacksjs/video@0.75.219", "", { "dependencies": { "@ts-videos/mp4": "^0.1.2", "@ts-videos/webm": "^0.1.2", "ts-video-player": "^0.1.3", "ts-videos": "^0.1.2" } }, "sha512-HTrykY1uBvWcLoqG5Pz32Nww6k5EPJi3IhzT9RPMWa1q1FVjsuG+pNN9RnDsD9k2HAT/pMtzrFAo6/0upz/jNA=="], | |
| 278 | "@stacksjs/video": ["@stacksjs/video@0.75.221", "", { "dependencies": { "@ts-videos/mp4": "^0.1.2", "@ts-videos/webm": "^0.1.2", "ts-video-player": "^0.1.3", "ts-videos": "^0.1.2" } }, "sha512-f+GdcUHNtBc7kYOU3ecVJ2CEpFnSymCEr6tCOJYm33DDSrfUhOdP8t4nUIpgu4LtgLXy/YcZE8+QPs37Se6Udw=="], | |
| 279 | 279 | |
| 280 | "@stacksjs/whois": ["@stacksjs/whois@0.75.219", "", { "dependencies": { "@stacksjs/logging": "0.75.219" } }, "sha512-WqLG5P/QZa78YKyhghiv8vc7GGkIHyNr+PWEuRnQz3pCVELIcf5swahlOQoZtyaqC2P5xuKC9c5vm4VYOJ4MXw=="], | |
| 280 | "@stacksjs/whois": ["@stacksjs/whois@0.75.221", "", { "dependencies": { "@stacksjs/logging": "0.75.221" } }, "sha512-S7bIJZ5/IOnntL1s2zUkVRdsbF/1tK5tKdWjfCH+0e4kauYwsaDwW/k513VWocQQnMZMCvchNHUhbZxzyBWVkA=="], | |
| 281 | 281 | |
| 282 | 282 | "@ts-analytics/tracking": ["@ts-analytics/tracking@0.1.17", "", { "dependencies": { "bun-query-builder": "^0.3.0", "dynamodb-tooling": "^0.3.2", "ts-countries": "^0.1.2" }, "peerDependencies": { "@nuxt/kit": "^3.0.0", "@nuxt/schema": "^3.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@nuxt/kit", "@nuxt/schema", "vue"], "bin": { "analytics": "./bin/cli.ts" } }, "sha512-E6okdowDJM+mS0pAEaAC1R1xfX7jUIdZ7GoWyKSelZRPRky2w18ukOu2ixoF+g6h15kYF5pBURkz1TbadCtYvg=="], |
| 283 | 283 | |
| @@ -337,17 +337,17 @@ | ||
| 337 | 337 | |
| 338 | 338 | "bun-plugin-dtsx": ["bun-plugin-dtsx@0.11.19", "", { "dependencies": { "@stacksjs/dtsx": "0.11.19" } }, "sha512-9itJqTxzONupXIS1Bwp/AqgXWrgSeHe/j0QyFewJ+//pL2Id+RoWFbMLpl42v2ucKgSFknsmAbLjn7wJkVx/vw=="], |
| 339 | 339 | |
| 340 | "bun-plugin-stx": ["bun-plugin-stx@0.2.563", "", { "dependencies": { "@stacksjs/stx": "0.2.563", "stx-router": "0.2.563" }, "bin": { "serve": "./dist/serve.js", "stx-serve": "./dist/serve.js" } }, "sha512-o1lCCkNY/tgjf4esi0IyyWw8BlGNEtI0TyOhtbUxRZi5CjQOBEfDCe9kl/kcm+9TSJacLZUW92HDa3cA9uhZqg=="], | |
| 340 | "bun-plugin-stx": ["bun-plugin-stx@0.2.573", "", { "dependencies": { "@stacksjs/stx": "0.2.573", "stx-router": "0.2.573" }, "bin": { "serve": "./dist/serve.js", "stx-serve": "./dist/serve.js" } }, "sha512-9FHWtkN+b9awtnBnMR6EIL0StIeQoOloBIu0izbrp7zn71BSgzYvTwJYcT1Z2X3jc0x2ecJEnfNevxqv4b7ulQ=="], | |
| 341 | 341 | |
| 342 | 342 | "bun-query-builder": ["bun-query-builder@0.3.3", "", { "dependencies": { "@stacksjs/clapp": "^0.2.16", "@stacksjs/ts-faker": "^0.1.8", "@stacksjs/ts-validation": "^0.5.6", "dynamodb-tooling": "^0.3.6" }, "bin": { "qb": "./dist/bin/cli.js", "qbx": "./dist/bin/cli.js", "query-builder": "./dist/bin/cli.js" } }, "sha512-MzCmuRel0l9gmnHZBXqBera93jNthVQAnTHjZ5IGOqNyRSAB3Wm/9cGLu9zFPQK4zzkNXFsmP+maNwbenzkXzA=="], |
| 343 | 343 | |
| 344 | 344 | "bun-types": ["bun-types@1.4.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-bxV1FgK7yBIzjRe5zBozIM4Bem11ZJcCXSrjWRG3YWLt8yFDePu4cLjpebO8OvPeIE9trbyPF4fuj3Cia4Fj3w=="], |
| 345 | 345 | |
| 346 | "bunfig": ["bunfig@0.15.19", "", { "dependencies": { "@stacksjs/clarity": "^0.3.24" }, "bin": { "bunfig": "./dist/cli.js" } }, "sha512-Ig302A74YUmagywXX1Dl8hXNdwjq/MIlB1TBVRduO6kGTMjkFQsxS3LimTFBNEThWypc/JS6cv4+TLeRAl2RGw=="], | |
| 346 | "bunfig": ["bunfig@0.15.21", "", { "dependencies": { "@stacksjs/clarity": "^0.3.24" }, "bin": { "bunfig": "./dist/cli.js" } }, "sha512-CuOLpPGC+ESF+Y0V8g4B04Y5JR49sZpYSye34n+vfsk2wGJu0wyBYVk9dICLtJZq6yfaaqoPc7MkLSkFdJ9akw=="], | |
| 347 | 347 | |
| 348 | 348 | "clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="], |
| 349 | 349 | |
| 350 | "craft-native": ["craft-native@0.0.237", "", { "bin": { "craft-sdk": "dist/cli.js" } }, "sha512-TALWq4Zv0FWGgO9rk71ASKHjVoodKFAKeV+FEOPP0qvQ9+PxPK9hikwY2XeTY018O1bVlIbE6AnW5YxevisBog=="], | |
| 350 | "craft-native": ["craft-native@0.0.244", "", { "bin": { "craft-sdk": "dist/cli.js" } }, "sha512-sdlzNDKrszlzW1UyIQOgb5cy3Kd7O1DGVo9pj8dO49MRu0ndznFOG8gLAyZ4OSdlKI2Gl+zj0KSm0xyzyvJV9g=="], | |
| 351 | 351 | |
| 352 | 352 | "cz-git": ["cz-git@1.14.0", "", {}, "sha512-QjwlXZrGWsXQkyregBHhA2qNNGB2TCuf0ovkW1Q5OLgdJDs9Tq1yahfKpSMLyqOBjVRuv1iHBTDP83zJDBlJdg=="], |
| 353 | 353 | |
| @@ -369,9 +369,9 @@ | ||
| 369 | 369 | |
| 370 | 370 | "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], |
| 371 | 371 | |
| 372 | "stacks": ["stacks@0.75.219", "", { "dependencies": { "@stacksjs/actions": "0.75.219", "@stacksjs/ai": "0.75.219", "@stacksjs/alias": "0.75.219", "@stacksjs/analytics": "0.75.219", "@stacksjs/api": "0.75.219", "@stacksjs/arrays": "0.75.219", "@stacksjs/audio": "0.75.219", "@stacksjs/auth": "0.75.219", "@stacksjs/browser": "0.75.219", "@stacksjs/browser-extension": "0.75.219", "@stacksjs/buddy": "0.75.219", "@stacksjs/build": "0.75.219", "@stacksjs/cache": "0.75.219", "@stacksjs/calendar-api": "0.75.219", "@stacksjs/charts": "0.75.219", "@stacksjs/chat": "0.75.219", "@stacksjs/cli": "0.75.219", "@stacksjs/cloud": "0.75.219", "@stacksjs/cms": "0.75.219", "@stacksjs/collections": "0.75.219", "@stacksjs/commerce": "0.75.219", "@stacksjs/composables": "0.75.219", "@stacksjs/config": "0.75.219", "@stacksjs/cron": "0.75.219", "@stacksjs/database": "0.75.219", "@stacksjs/datetime": "0.75.219", "@stacksjs/defaults": "0.75.219", "@stacksjs/desktop": "^0.2.534", "@stacksjs/dns": "0.75.219", "@stacksjs/docs": "0.75.219", "@stacksjs/email": "0.75.219", "@stacksjs/enums": "0.75.219", "@stacksjs/env": "0.75.219", "@stacksjs/error-handling": "0.75.219", "@stacksjs/events": "0.75.219", "@stacksjs/faker": "0.75.219", "@stacksjs/feature-flags": "0.75.219", "@stacksjs/forms": "0.75.219", "@stacksjs/git": "0.75.219", "@stacksjs/github": "0.75.219", "@stacksjs/health": "0.75.219", "@stacksjs/http": "0.75.219", "@stacksjs/i18n": "0.75.219", "@stacksjs/image": "0.75.219", "@stacksjs/lint": "0.75.219", "@stacksjs/logging": "0.75.219", "@stacksjs/mobile": "0.75.219", "@stacksjs/newsletter": "0.75.219", "@stacksjs/notifications": "0.75.219", "@stacksjs/objects": "0.75.219", "@stacksjs/orm": "0.75.219", "@stacksjs/path": "0.75.219", "@stacksjs/payments": "0.75.219", "@stacksjs/push": "0.75.219", "@stacksjs/query-builder": "0.75.219", "@stacksjs/queue": "0.75.219", "@stacksjs/realtime": "0.75.219", "@stacksjs/registry": "0.75.219", "@stacksjs/repl": "0.75.219", "@stacksjs/router": "0.75.219", "@stacksjs/scheduler": "0.75.219", "@stacksjs/search-engine": "0.75.219", "@stacksjs/security": "0.75.219", "@stacksjs/server": "0.75.219", "@stacksjs/shell": "0.75.219", "@stacksjs/sites": "0.75.219", "@stacksjs/skills": "0.75.219", "@stacksjs/slug": "0.75.219", "@stacksjs/sms": "0.75.219", "@stacksjs/socials": "0.75.219", "@stacksjs/storage": "0.75.219", "@stacksjs/strings": "0.75.219", "@stacksjs/testing": "0.75.219", "@stacksjs/tinker": "0.75.219", "@stacksjs/tlsx": "^0.13.19", "@stacksjs/ts-cloud": "^0.16.52", "@stacksjs/tunnel": "0.75.219", "@stacksjs/types": "0.75.219", "@stacksjs/ui": "0.75.219", "@stacksjs/utils": "0.75.219", "@stacksjs/validation": "0.75.219", "@stacksjs/video": "0.75.219", "@stacksjs/whois": "0.75.219", "bun-query-builder": "^0.3.8" } }, "sha512-3NdYXURJ14BDEFiyJEq9UxOXLQYfmMe4zK5cWB8+6XuAurFBPnZyn4xwYpX51ta+v/g+9upfk2pmhRJSAEGX0A=="], | |
| 372 | "stacks": ["stacks@0.75.221", "", { "dependencies": { "@stacksjs/actions": "0.75.221", "@stacksjs/ai": "0.75.221", "@stacksjs/alias": "0.75.221", "@stacksjs/analytics": "0.75.221", "@stacksjs/api": "0.75.221", "@stacksjs/arrays": "0.75.221", "@stacksjs/audio": "0.75.221", "@stacksjs/auth": "0.75.221", "@stacksjs/browser": "0.75.221", "@stacksjs/browser-extension": "0.75.221", "@stacksjs/buddy": "0.75.221", "@stacksjs/build": "0.75.221", "@stacksjs/cache": "0.75.221", "@stacksjs/calendar-api": "0.75.221", "@stacksjs/charts": "0.75.221", "@stacksjs/chat": "0.75.221", "@stacksjs/cli": "0.75.221", "@stacksjs/cloud": "0.75.221", "@stacksjs/cms": "0.75.221", "@stacksjs/collections": "0.75.221", "@stacksjs/commerce": "0.75.221", "@stacksjs/composables": "0.75.221", "@stacksjs/config": "0.75.221", "@stacksjs/cron": "0.75.221", "@stacksjs/database": "0.75.221", "@stacksjs/datetime": "0.75.221", "@stacksjs/defaults": "0.75.221", "@stacksjs/desktop": "^0.2.534", "@stacksjs/dns": "0.75.221", "@stacksjs/docs": "0.75.221", "@stacksjs/email": "0.75.221", "@stacksjs/enums": "0.75.221", "@stacksjs/env": "0.75.221", "@stacksjs/error-handling": "0.75.221", "@stacksjs/events": "0.75.221", "@stacksjs/faker": "0.75.221", "@stacksjs/feature-flags": "0.75.221", "@stacksjs/forms": "0.75.221", "@stacksjs/git": "0.75.221", "@stacksjs/github": "0.75.221", "@stacksjs/health": "0.75.221", "@stacksjs/http": "0.75.221", "@stacksjs/i18n": "0.75.221", "@stacksjs/image": "0.75.221", "@stacksjs/lint": "0.75.221", "@stacksjs/logging": "0.75.221", "@stacksjs/mobile": "0.75.221", "@stacksjs/newsletter": "0.75.221", "@stacksjs/notifications": "0.75.221", "@stacksjs/objects": "0.75.221", "@stacksjs/orm": "0.75.221", "@stacksjs/path": "0.75.221", "@stacksjs/payments": "0.75.221", "@stacksjs/push": "0.75.221", "@stacksjs/query-builder": "0.75.221", "@stacksjs/queue": "0.75.221", "@stacksjs/realtime": "0.75.221", "@stacksjs/registry": "0.75.221", "@stacksjs/repl": "0.75.221", "@stacksjs/router": "0.75.221", "@stacksjs/scheduler": "0.75.221", "@stacksjs/search-engine": "0.75.221", "@stacksjs/security": "0.75.221", "@stacksjs/server": "0.75.221", "@stacksjs/shell": "0.75.221", "@stacksjs/sites": "0.75.221", "@stacksjs/skills": "0.75.221", "@stacksjs/slug": "0.75.221", "@stacksjs/sms": "0.75.221", "@stacksjs/socials": "0.75.221", "@stacksjs/storage": "0.75.221", "@stacksjs/strings": "0.75.221", "@stacksjs/testing": "0.75.221", "@stacksjs/tinker": "0.75.221", "@stacksjs/tlsx": "^0.13.19", "@stacksjs/ts-cloud": "^0.16.52", "@stacksjs/tunnel": "0.75.221", "@stacksjs/types": "0.75.221", "@stacksjs/ui": "0.75.221", "@stacksjs/utils": "0.75.221", "@stacksjs/validation": "0.75.221", "@stacksjs/video": "0.75.221", "@stacksjs/whois": "0.75.221", "bun-query-builder": "^0.3.8" } }, "sha512-O7T4OI0TbLNt8NkAlvfIDIyMBbXdlnryUc53xUVwY7YW5ecEk+eIz5X8xNdjro4GsfRd8M1xTjjTEICOxLBO0Q=="], | |
| 373 | 373 | |
| 374 | "stx-router": ["stx-router@0.2.563", "", {}, "sha512-po9NOFH13f0sTTrYr8yssQQmuCecH19UZR5hju3U/PR4bGYWfooN3qm/WAkTeRrbp/JfPbcFFEar1dMfNTzqDw=="], | |
| 374 | "stx-router": ["stx-router@0.2.573", "", {}, "sha512-hkS4VPVZSmFAVSCtw9r/eSVC+rSLfdvLeK9Ru/gIIEwXRJUZ/KCnFOg4Sl97kXFWT7msaU+q3hDW1WsoLRKTVw=="], | |
| 375 | 375 | |
| 376 | 376 | "ts-broadcasting": ["ts-broadcasting@0.0.11", "", { "bin": { "broadcast": "./dist/bin/cli.js" } }, "sha512-lqn/VGfW/JiXY8TMtzq4RLu2QlK6Q8oLQg/G9zLBtp5gFoO4dfxihyXPSr/elmGCvV9keC7RbLva6cMSBkhnQg=="], |
| 377 | 377 | |
| @@ -423,28 +423,38 @@ | ||
| 423 | 423 | |
| 424 | 424 | "very-happy-dom": ["very-happy-dom@0.3.5", "", {}, "sha512-uuyxaij6RPM15Znv22NwAK/rBJTHsWi7+EP4tmf7XetvpwSLtX7EYFkQRwYFlbiswHV07MiFnyfU2HK1xGdlQg=="], |
| 425 | 425 | |
| 426 | "@stacksjs/actions/bun-plugin-stx": ["bun-plugin-stx@0.2.559", "", { "dependencies": { "@stacksjs/stx": "0.2.559", "stx-router": "0.2.559" }, "bin": { "serve": "./dist/serve.js", "stx-serve": "./dist/serve.js" } }, "sha512-Vc115VL5P+pHgyKrutJ50qPGej+pBNyxcyWQJrBL6NLQfcvTbDZI6OOk/uEhg/yq+aXs2OlOW3zlyjLi+RJY+Q=="], | |
| 426 | "@cwcss/crosswind/bunfig": ["bunfig@0.15.19", "", { "dependencies": { "@stacksjs/clarity": "^0.3.24" }, "bin": { "bunfig": "./dist/cli.js" } }, "sha512-Ig302A74YUmagywXX1Dl8hXNdwjq/MIlB1TBVRduO6kGTMjkFQsxS3LimTFBNEThWypc/JS6cv4+TLeRAl2RGw=="], | |
| 427 | ||
| 428 | "@stacksjs/actions/bun-plugin-stx": ["bun-plugin-stx@0.2.567", "", { "dependencies": { "@stacksjs/stx": "0.2.567", "stx-router": "0.2.567" }, "bin": { "serve": "./dist/serve.js", "stx-serve": "./dist/serve.js" } }, "sha512-F2cDsc1SApgs4W80jgXtWm0l6YAKNPnGiC0SQ27jPQYg/nfpNssDmO5tCGwUiZ+xqTVOZJsDDT7GzeMDbagriQ=="], | |
| 427 | 429 | |
| 428 | 430 | "@stacksjs/browser/bun-query-builder": ["bun-query-builder@0.3.13", "", { "dependencies": { "@stacksjs/clapp": "^0.2.16", "@stacksjs/ts-faker": "^0.1.8", "@stacksjs/ts-validation": "^0.5.6", "dynamodb-tooling": "^0.3.6" }, "bin": { "qb": "./dist/bin/cli.js", "qbx": "./dist/bin/cli.js", "query-builder": "./dist/bin/cli.js" } }, "sha512-9c4IJMILOQ9TYyk6PWWoFgD3tpGA+1+oBIOcj6hIH4duSYRC1qgI5gV6pHg2B9pyJik9fwRuM40TjQZ8GiDIZw=="], |
| 429 | 431 | |
| 430 | "@stacksjs/browser-extension/bun-plugin-stx": ["bun-plugin-stx@0.2.559", "", { "dependencies": { "@stacksjs/stx": "0.2.559", "stx-router": "0.2.559" }, "bin": { "serve": "./dist/serve.js", "stx-serve": "./dist/serve.js" } }, "sha512-Vc115VL5P+pHgyKrutJ50qPGej+pBNyxcyWQJrBL6NLQfcvTbDZI6OOk/uEhg/yq+aXs2OlOW3zlyjLi+RJY+Q=="], | |
| 432 | "@stacksjs/browser-extension/bun-plugin-stx": ["bun-plugin-stx@0.2.567", "", { "dependencies": { "@stacksjs/stx": "0.2.567", "stx-router": "0.2.567" }, "bin": { "serve": "./dist/serve.js", "stx-serve": "./dist/serve.js" } }, "sha512-F2cDsc1SApgs4W80jgXtWm0l6YAKNPnGiC0SQ27jPQYg/nfpNssDmO5tCGwUiZ+xqTVOZJsDDT7GzeMDbagriQ=="], | |
| 433 | ||
| 434 | "@stacksjs/buddy/bun-plugin-stx": ["bun-plugin-stx@0.2.567", "", { "dependencies": { "@stacksjs/stx": "0.2.567", "stx-router": "0.2.567" }, "bin": { "serve": "./dist/serve.js", "stx-serve": "./dist/serve.js" } }, "sha512-F2cDsc1SApgs4W80jgXtWm0l6YAKNPnGiC0SQ27jPQYg/nfpNssDmO5tCGwUiZ+xqTVOZJsDDT7GzeMDbagriQ=="], | |
| 431 | 435 | |
| 432 | "@stacksjs/buddy/bun-plugin-stx": ["bun-plugin-stx@0.2.559", "", { "dependencies": { "@stacksjs/stx": "0.2.559", "stx-router": "0.2.559" }, "bin": { "serve": "./dist/serve.js", "stx-serve": "./dist/serve.js" } }, "sha512-Vc115VL5P+pHgyKrutJ50qPGej+pBNyxcyWQJrBL6NLQfcvTbDZI6OOk/uEhg/yq+aXs2OlOW3zlyjLi+RJY+Q=="], | |
| 436 | "@stacksjs/bumpx/bunfig": ["bunfig@0.15.19", "", { "dependencies": { "@stacksjs/clarity": "^0.3.24" }, "bin": { "bunfig": "./dist/cli.js" } }, "sha512-Ig302A74YUmagywXX1Dl8hXNdwjq/MIlB1TBVRduO6kGTMjkFQsxS3LimTFBNEThWypc/JS6cv4+TLeRAl2RGw=="], | |
| 433 | 437 | |
| 434 | 438 | "@stacksjs/bunpress/@stacksjs/stx": ["@stacksjs/stx@0.2.520", "", { "dependencies": { "@stacksjs/clapp": "^0.2.16", "@stacksjs/desktop": "0.2.520", "@stacksjs/sanitizer": "0.2.520", "@stacksjs/ts-css": "^0.3.11", "@stacksjs/ts-i18n": "^0.1.11", "bun-plugin-stx": "0.2.520", "bunfig": "^0.15.19", "stx-router": "0.2.520", "ts-broadcasting": "^0.0.11", "ts-images": "^0.2.22", "ts-syntax-highlighter": "^0.2.1" }, "bin": { "stx": "./dist/cli.js" } }, "sha512-XDatWtXZVid/RGL8YX9oOzUl0h1jB+2fPXH9O8VJ4G2+Mp+D5T1JwzHu+Nxu8RdSggCFq4/5PZ5xfgsU1/IIYA=="], |
| 435 | 439 | |
| 440 | "@stacksjs/bunpress/bunfig": ["bunfig@0.15.19", "", { "dependencies": { "@stacksjs/clarity": "^0.3.24" }, "bin": { "bunfig": "./dist/cli.js" } }, "sha512-Ig302A74YUmagywXX1Dl8hXNdwjq/MIlB1TBVRduO6kGTMjkFQsxS3LimTFBNEThWypc/JS6cv4+TLeRAl2RGw=="], | |
| 441 | ||
| 442 | "@stacksjs/clarity/bunfig": ["bunfig@0.15.19", "", { "dependencies": { "@stacksjs/clarity": "^0.3.24" }, "bin": { "bunfig": "./dist/cli.js" } }, "sha512-Ig302A74YUmagywXX1Dl8hXNdwjq/MIlB1TBVRduO6kGTMjkFQsxS3LimTFBNEThWypc/JS6cv4+TLeRAl2RGw=="], | |
| 443 | ||
| 436 | 444 | "@stacksjs/components/@stacksjs/stx": ["@stacksjs/stx@0.2.556", "", { "dependencies": { "@stacksjs/clapp": "^0.2.16", "@stacksjs/desktop": "0.2.556", "@stacksjs/sanitizer": "0.2.556", "@stacksjs/ts-css": "^0.3.11", "@stacksjs/ts-i18n": "^0.1.11", "bun-plugin-stx": "0.2.556", "bunfig": "^0.15.19", "stx-router": "0.2.556", "ts-broadcasting": "^0.0.11", "ts-images": "^0.2.22", "ts-syntax-highlighter": "^0.2.1" }, "bin": { "stx": "./dist/cli.js" } }, "sha512-SpOB2NIiIS33i7XuFT029JtmopdkJb69Y1uQfH1usPQJkwrbMa4UROjekLlpKhx2dMocYqEu9n1qs2fda3DMww=="], |
| 437 | 445 | |
| 438 | 446 | "@stacksjs/components/bun-plugin-stx": ["bun-plugin-stx@0.2.556", "", { "dependencies": { "@stacksjs/stx": "0.2.556", "stx-router": "0.2.556" }, "bin": { "serve": "./dist/serve.js", "stx-serve": "./dist/serve.js" } }, "sha512-hWyPA3v0DKZZtwaZ6FHlThDTMuG1PtQmQeeKuj6J/xd1Gqbd9OUO3PXieLU5Pwu9YYHPpzGEZIG7kQYp0qKdSQ=="], |
| 439 | 447 | |
| 440 | 448 | "@stacksjs/database/bun-query-builder": ["bun-query-builder@0.3.13", "", { "dependencies": { "@stacksjs/clapp": "^0.2.16", "@stacksjs/ts-faker": "^0.1.8", "@stacksjs/ts-validation": "^0.5.6", "dynamodb-tooling": "^0.3.6" }, "bin": { "qb": "./dist/bin/cli.js", "qbx": "./dist/bin/cli.js", "query-builder": "./dist/bin/cli.js" } }, "sha512-9c4IJMILOQ9TYyk6PWWoFgD3tpGA+1+oBIOcj6hIH4duSYRC1qgI5gV6pHg2B9pyJik9fwRuM40TjQZ8GiDIZw=="], |
| 441 | 449 | |
| 442 | "@stacksjs/defaults/@stacksjs/sanitizer": ["@stacksjs/sanitizer@0.2.559", "", {}, "sha512-9Gg3Qc1eGkI3kmAgPk5IwGI1rIyvXLlGvX7SxwitLbdXu8mScmPPbVWY3KV+1fhCS7xvqZJ1vnRZT4Tk++EoZw=="], | |
| 450 | "@stacksjs/defaults/@stacksjs/sanitizer": ["@stacksjs/sanitizer@0.2.567", "", {}, "sha512-gMq1MqsmLTqZ/y173wAi5/7mrfByPFBJH18bDFzoHV/GkcDuy5GwRbpkmNWiuLEBlUArLutautYHRZBiKeyYPg=="], | |
| 443 | 451 | |
| 444 | 452 | "@stacksjs/error-handling/@bughq/sdk": ["@bughq/sdk@0.3.0", "", {}, "sha512-7hHjXJNUrMK/G5w+arw2b8IM2dlV1pU6i7KOScvO9+K/QbHDUjsz/CousavHEh7CE0Puo1TavbG7hl3Rkt1LOA=="], |
| 445 | 453 | |
| 446 | 454 | "@stacksjs/image/@stacksjs/ts-svg": ["@stacksjs/ts-svg@0.1.5", "", { "dependencies": { "@stacksjs/ts-css": "^0.3.8", "@stacksjs/ts-png": "^0.1.2" }, "bin": { "svg": "./dist/cli.js" } }, "sha512-XH6N3+l6NPerascxFO3yDCH6EctIgVaKHGF6UWlFe1mh4IMGFGn+k82+i1bNPtqnNO6JVOIOPG4E4zufFPVW6w=="], |
| 447 | 455 | |
| 456 | "@stacksjs/logsmith/bunfig": ["bunfig@0.15.19", "", { "dependencies": { "@stacksjs/clarity": "^0.3.24" }, "bin": { "bunfig": "./dist/cli.js" } }, "sha512-Ig302A74YUmagywXX1Dl8hXNdwjq/MIlB1TBVRduO6kGTMjkFQsxS3LimTFBNEThWypc/JS6cv4+TLeRAl2RGw=="], | |
| 457 | ||
| 448 | 458 | "@stacksjs/logsmith/pickier": ["pickier@0.1.65", "", { "dependencies": { "@stacksjs/clapp": "^0.2.3", "@stacksjs/clarity": "^0.3.24" }, "optionalDependencies": { "@stacksjs/ts-spell-check": "^0.1.0" }, "bin": { "pickier": "./dist/bin/cli.js" } }, "sha512-hpGKvrWwzzHeV3hMErtnZoGRD/5Wmvy9sKxxHAWvMMdmdzr2DMtuk2T8Vkr8ZoUCNeTKRRPgHTMHg0oCNxDnDA=="], |
| 449 | 459 | |
| 450 | 460 | "@stacksjs/mobile/@bughq/sdk": ["@bughq/sdk@0.3.0", "", {}, "sha512-7hHjXJNUrMK/G5w+arw2b8IM2dlV1pU6i7KOScvO9+K/QbHDUjsz/CousavHEh7CE0Puo1TavbG7hl3Rkt1LOA=="], |
| @@ -455,25 +465,39 @@ | ||
| 455 | 465 | |
| 456 | 466 | "@stacksjs/rpx/ts-images": ["ts-images@0.2.20", "", { "dependencies": { "@stacksjs/ts-avif": "^0.1.4", "@stacksjs/ts-bmp": "^0.1.0", "@stacksjs/ts-png": "^0.1.2", "@stacksjs/ts-svg": "^0.1.1", "@stacksjs/ts-webp": "^0.1.2", "ts-gif": "^0.1.2", "ts-jpeg": "^0.3.1" }, "bin": { "imgx": "./dist/bin/cli.js" } }, "sha512-yKjBq9To98gH+XhH3E/s9vdaMYUTicwBeujcQbODKFFrHjw4mflwn4jJH5tyx0o0KokPCv6K6fXEIfRwWrxAiw=="], |
| 457 | 467 | |
| 468 | "@stacksjs/ts-auth/bunfig": ["bunfig@0.15.19", "", { "dependencies": { "@stacksjs/clarity": "^0.3.24" }, "bin": { "bunfig": "./dist/cli.js" } }, "sha512-Ig302A74YUmagywXX1Dl8hXNdwjq/MIlB1TBVRduO6kGTMjkFQsxS3LimTFBNEThWypc/JS6cv4+TLeRAl2RGw=="], | |
| 469 | ||
| 470 | "@stacksjs/ts-cache/bunfig": ["bunfig@0.15.19", "", { "dependencies": { "@stacksjs/clarity": "^0.3.24" }, "bin": { "bunfig": "./dist/cli.js" } }, "sha512-Ig302A74YUmagywXX1Dl8hXNdwjq/MIlB1TBVRduO6kGTMjkFQsxS3LimTFBNEThWypc/JS6cv4+TLeRAl2RGw=="], | |
| 471 | ||
| 472 | "@stacksjs/ts-css/bunfig": ["bunfig@0.15.19", "", { "dependencies": { "@stacksjs/clarity": "^0.3.24" }, "bin": { "bunfig": "./dist/cli.js" } }, "sha512-Ig302A74YUmagywXX1Dl8hXNdwjq/MIlB1TBVRduO6kGTMjkFQsxS3LimTFBNEThWypc/JS6cv4+TLeRAl2RGw=="], | |
| 473 | ||
| 474 | "@stacksjs/ts-i18n/bunfig": ["bunfig@0.15.19", "", { "dependencies": { "@stacksjs/clarity": "^0.3.24" }, "bin": { "bunfig": "./dist/cli.js" } }, "sha512-Ig302A74YUmagywXX1Dl8hXNdwjq/MIlB1TBVRduO6kGTMjkFQsxS3LimTFBNEThWypc/JS6cv4+TLeRAl2RGw=="], | |
| 475 | ||
| 458 | 476 | "@stacksjs/ts-svg/@stacksjs/ts-css": ["@stacksjs/ts-css@0.1.4", "", { "dependencies": { "bunfig": "^0.15.6" }, "bin": { "ts-css": "./dist/bin/cli.js" } }, "sha512-QvHvgagJ6kQY3+zY37andUEJFKq/v4v2JpDCrvVdbX3TYCLuRQExZl8Qf3ai+Fl9BN2oWikH+DfKSs5HuEZrGQ=="], |
| 459 | 477 | |
| 460 | 478 | "@stacksjs/types/bun-query-builder": ["bun-query-builder@0.3.13", "", { "dependencies": { "@stacksjs/clapp": "^0.2.16", "@stacksjs/ts-faker": "^0.1.8", "@stacksjs/ts-validation": "^0.5.6", "dynamodb-tooling": "^0.3.6" }, "bin": { "qb": "./dist/bin/cli.js", "qbx": "./dist/bin/cli.js", "query-builder": "./dist/bin/cli.js" } }, "sha512-9c4IJMILOQ9TYyk6PWWoFgD3tpGA+1+oBIOcj6hIH4duSYRC1qgI5gV6pHg2B9pyJik9fwRuM40TjQZ8GiDIZw=="], |
| 461 | 479 | |
| 462 | "stacks/@stacksjs/desktop": ["@stacksjs/desktop@0.2.559", "", { "dependencies": { "craft-native": ">=0.0.111" } }, "sha512-0Mt21QCbWiV3EwPGlnCniO03+J3ysY64F8fDf4t/eQilH34B4l75fDCd2SGILHedjphkEd43pyxY4FBj9OQxCQ=="], | |
| 480 | "localtunnels/bunfig": ["bunfig@0.15.19", "", { "dependencies": { "@stacksjs/clarity": "^0.3.24" }, "bin": { "bunfig": "./dist/cli.js" } }, "sha512-Ig302A74YUmagywXX1Dl8hXNdwjq/MIlB1TBVRduO6kGTMjkFQsxS3LimTFBNEThWypc/JS6cv4+TLeRAl2RGw=="], | |
| 481 | ||
| 482 | "stacks/@stacksjs/desktop": ["@stacksjs/desktop@0.2.567", "", { "dependencies": { "craft-native": ">=0.0.111" } }, "sha512-g6X8P8l4A8GW3gNENBl9x7fuN0ZUZZqflRlqQXQ3SGyIWl0rCE/mWxqr7wg5sWT8R4Y30OXMBgz6AIQyILxlIg=="], | |
| 463 | 483 | |
| 464 | 484 | "stacks/bun-query-builder": ["bun-query-builder@0.3.13", "", { "dependencies": { "@stacksjs/clapp": "^0.2.16", "@stacksjs/ts-faker": "^0.1.8", "@stacksjs/ts-validation": "^0.5.6", "dynamodb-tooling": "^0.3.6" }, "bin": { "qb": "./dist/bin/cli.js", "qbx": "./dist/bin/cli.js", "query-builder": "./dist/bin/cli.js" } }, "sha512-9c4IJMILOQ9TYyk6PWWoFgD3tpGA+1+oBIOcj6hIH4duSYRC1qgI5gV6pHg2B9pyJik9fwRuM40TjQZ8GiDIZw=="], |
| 465 | 485 | |
| 466 | "@stacksjs/actions/bun-plugin-stx/@stacksjs/stx": ["@stacksjs/stx@0.2.559", "", { "dependencies": { "@stacksjs/clapp": "^0.2.16", "@stacksjs/desktop": "0.2.559", "@stacksjs/sanitizer": "0.2.559", "@stacksjs/ts-css": "^0.3.11", "@stacksjs/ts-i18n": "^0.1.11", "bun-plugin-stx": "0.2.559", "bunfig": "^0.15.19", "stx-router": "0.2.559", "ts-broadcasting": "^0.0.11", "ts-images": "^0.2.22", "ts-syntax-highlighter": "^0.2.1" }, "bin": { "stx": "./dist/cli.js" } }, "sha512-JLcCChDUa56FxauagTmm3V2drLBC8KejGznFGCBT2fM8r5OOqUg5qqlTp8lbCM0Wl4ClAR9rd+Hu58BrcR0gRA=="], | |
| 486 | "ts-videos/bunfig": ["bunfig@0.15.19", "", { "dependencies": { "@stacksjs/clarity": "^0.3.24" }, "bin": { "bunfig": "./dist/cli.js" } }, "sha512-Ig302A74YUmagywXX1Dl8hXNdwjq/MIlB1TBVRduO6kGTMjkFQsxS3LimTFBNEThWypc/JS6cv4+TLeRAl2RGw=="], | |
| 487 | ||
| 488 | "ts-web-scraper/bunfig": ["bunfig@0.15.19", "", { "dependencies": { "@stacksjs/clarity": "^0.3.24" }, "bin": { "bunfig": "./dist/cli.js" } }, "sha512-Ig302A74YUmagywXX1Dl8hXNdwjq/MIlB1TBVRduO6kGTMjkFQsxS3LimTFBNEThWypc/JS6cv4+TLeRAl2RGw=="], | |
| 467 | 489 | |
| 468 | "@stacksjs/actions/bun-plugin-stx/stx-router": ["stx-router@0.2.559", "", {}, "sha512-uhnWMGZL5E1IDbiUzRlbvOurP5Mn5mJi52/R13ssyE0PV5rKyUXeY4YED7YXuFkuDZwPudxjOe1UdpKgIpOceg=="], | |
| 490 | "@stacksjs/actions/bun-plugin-stx/@stacksjs/stx": ["@stacksjs/stx@0.2.567", "", { "dependencies": { "@stacksjs/clapp": "^0.2.16", "@stacksjs/desktop": "0.2.567", "@stacksjs/sanitizer": "0.2.567", "@stacksjs/ts-css": "^0.3.11", "@stacksjs/ts-i18n": "^0.1.11", "bun-plugin-stx": "0.2.567", "bunfig": "^0.15.19", "stx-router": "0.2.567", "ts-broadcasting": "^0.0.11", "ts-images": "^0.2.22", "ts-syntax-highlighter": "^0.2.1" }, "bin": { "stx": "./dist/cli.js" } }, "sha512-qH5sXYgf8Bwc4LsA6zkA37sz8jU36xeb1L/Nxrb9VD4yfI9vYLUDWAQRq2qophwjEan1q9zim3goWFJ8vK05sw=="], | |
| 469 | 491 | |
| 470 | "@stacksjs/browser-extension/bun-plugin-stx/@stacksjs/stx": ["@stacksjs/stx@0.2.559", "", { "dependencies": { "@stacksjs/clapp": "^0.2.16", "@stacksjs/desktop": "0.2.559", "@stacksjs/sanitizer": "0.2.559", "@stacksjs/ts-css": "^0.3.11", "@stacksjs/ts-i18n": "^0.1.11", "bun-plugin-stx": "0.2.559", "bunfig": "^0.15.19", "stx-router": "0.2.559", "ts-broadcasting": "^0.0.11", "ts-images": "^0.2.22", "ts-syntax-highlighter": "^0.2.1" }, "bin": { "stx": "./dist/cli.js" } }, "sha512-JLcCChDUa56FxauagTmm3V2drLBC8KejGznFGCBT2fM8r5OOqUg5qqlTp8lbCM0Wl4ClAR9rd+Hu58BrcR0gRA=="], | |
| 492 | "@stacksjs/actions/bun-plugin-stx/stx-router": ["stx-router@0.2.567", "", {}, "sha512-EwpUt/VwzPDNRWrjf6M/Rzxc81asNDNUZFw8PmHODzFGdmL1gi7oJtZZok8zQLvJpDO881Wk2rDm6C+e0WNTQw=="], | |
| 471 | 493 | |
| 472 | "@stacksjs/browser-extension/bun-plugin-stx/stx-router": ["stx-router@0.2.559", "", {}, "sha512-uhnWMGZL5E1IDbiUzRlbvOurP5Mn5mJi52/R13ssyE0PV5rKyUXeY4YED7YXuFkuDZwPudxjOe1UdpKgIpOceg=="], | |
| 494 | "@stacksjs/browser-extension/bun-plugin-stx/@stacksjs/stx": ["@stacksjs/stx@0.2.567", "", { "dependencies": { "@stacksjs/clapp": "^0.2.16", "@stacksjs/desktop": "0.2.567", "@stacksjs/sanitizer": "0.2.567", "@stacksjs/ts-css": "^0.3.11", "@stacksjs/ts-i18n": "^0.1.11", "bun-plugin-stx": "0.2.567", "bunfig": "^0.15.19", "stx-router": "0.2.567", "ts-broadcasting": "^0.0.11", "ts-images": "^0.2.22", "ts-syntax-highlighter": "^0.2.1" }, "bin": { "stx": "./dist/cli.js" } }, "sha512-qH5sXYgf8Bwc4LsA6zkA37sz8jU36xeb1L/Nxrb9VD4yfI9vYLUDWAQRq2qophwjEan1q9zim3goWFJ8vK05sw=="], | |
| 473 | 495 | |
| 474 | "@stacksjs/buddy/bun-plugin-stx/@stacksjs/stx": ["@stacksjs/stx@0.2.559", "", { "dependencies": { "@stacksjs/clapp": "^0.2.16", "@stacksjs/desktop": "0.2.559", "@stacksjs/sanitizer": "0.2.559", "@stacksjs/ts-css": "^0.3.11", "@stacksjs/ts-i18n": "^0.1.11", "bun-plugin-stx": "0.2.559", "bunfig": "^0.15.19", "stx-router": "0.2.559", "ts-broadcasting": "^0.0.11", "ts-images": "^0.2.22", "ts-syntax-highlighter": "^0.2.1" }, "bin": { "stx": "./dist/cli.js" } }, "sha512-JLcCChDUa56FxauagTmm3V2drLBC8KejGznFGCBT2fM8r5OOqUg5qqlTp8lbCM0Wl4ClAR9rd+Hu58BrcR0gRA=="], | |
| 496 | "@stacksjs/browser-extension/bun-plugin-stx/stx-router": ["stx-router@0.2.567", "", {}, "sha512-EwpUt/VwzPDNRWrjf6M/Rzxc81asNDNUZFw8PmHODzFGdmL1gi7oJtZZok8zQLvJpDO881Wk2rDm6C+e0WNTQw=="], | |
| 475 | 497 | |
| 476 | "@stacksjs/buddy/bun-plugin-stx/stx-router": ["stx-router@0.2.559", "", {}, "sha512-uhnWMGZL5E1IDbiUzRlbvOurP5Mn5mJi52/R13ssyE0PV5rKyUXeY4YED7YXuFkuDZwPudxjOe1UdpKgIpOceg=="], | |
| 498 | "@stacksjs/buddy/bun-plugin-stx/@stacksjs/stx": ["@stacksjs/stx@0.2.567", "", { "dependencies": { "@stacksjs/clapp": "^0.2.16", "@stacksjs/desktop": "0.2.567", "@stacksjs/sanitizer": "0.2.567", "@stacksjs/ts-css": "^0.3.11", "@stacksjs/ts-i18n": "^0.1.11", "bun-plugin-stx": "0.2.567", "bunfig": "^0.15.19", "stx-router": "0.2.567", "ts-broadcasting": "^0.0.11", "ts-images": "^0.2.22", "ts-syntax-highlighter": "^0.2.1" }, "bin": { "stx": "./dist/cli.js" } }, "sha512-qH5sXYgf8Bwc4LsA6zkA37sz8jU36xeb1L/Nxrb9VD4yfI9vYLUDWAQRq2qophwjEan1q9zim3goWFJ8vK05sw=="], | |
| 499 | ||
| 500 | "@stacksjs/buddy/bun-plugin-stx/stx-router": ["stx-router@0.2.567", "", {}, "sha512-EwpUt/VwzPDNRWrjf6M/Rzxc81asNDNUZFw8PmHODzFGdmL1gi7oJtZZok8zQLvJpDO881Wk2rDm6C+e0WNTQw=="], | |
| 477 | 501 | |
| 478 | 502 | "@stacksjs/bunpress/@stacksjs/stx/@stacksjs/desktop": ["@stacksjs/desktop@0.2.520", "", { "dependencies": { "craft-native": ">=0.0.111" } }, "sha512-0LwPYuRqWP4yNgb7txzW5jikOTA3PVLje6TDOS4BLDiT3PojBg+aRt+YCEEZet4Rlu2yNHp4QzK1fALGfI137A=="], |
| 479 | 503 | |
| @@ -487,23 +511,33 @@ | ||
| 487 | 511 | |
| 488 | 512 | "@stacksjs/components/@stacksjs/stx/@stacksjs/sanitizer": ["@stacksjs/sanitizer@0.2.556", "", {}, "sha512-xm74lJChDP1NKUsL4aNDNqF4RhX5QWTogChphY7b2EbQdzPx3QwLdRbKt3bes2UDrP2Zw5cg4OoI0W+Jo3suVA=="], |
| 489 | 513 | |
| 514 | "@stacksjs/components/@stacksjs/stx/bunfig": ["bunfig@0.15.19", "", { "dependencies": { "@stacksjs/clarity": "^0.3.24" }, "bin": { "bunfig": "./dist/cli.js" } }, "sha512-Ig302A74YUmagywXX1Dl8hXNdwjq/MIlB1TBVRduO6kGTMjkFQsxS3LimTFBNEThWypc/JS6cv4+TLeRAl2RGw=="], | |
| 515 | ||
| 490 | 516 | "@stacksjs/components/@stacksjs/stx/stx-router": ["stx-router@0.2.556", "", {}, "sha512-gLx2kFDINgVx8emxGJ6f+vShLgAK2XTop/D0xMfdy6dK3Qal5tBeGMT9Vdti7KfL2N+oYTDLs4mVHby6pf12sg=="], |
| 491 | 517 | |
| 492 | 518 | "@stacksjs/components/bun-plugin-stx/stx-router": ["stx-router@0.2.556", "", {}, "sha512-gLx2kFDINgVx8emxGJ6f+vShLgAK2XTop/D0xMfdy6dK3Qal5tBeGMT9Vdti7KfL2N+oYTDLs4mVHby6pf12sg=="], |
| 493 | 519 | |
| 494 | 520 | "@stacksjs/rpx/ts-images/@stacksjs/ts-webp": ["@stacksjs/ts-webp@0.1.3", "", {}, "sha512-0PFmmBLKCxIc+8pNEwfvoXfS5uH47y7rXpVaGEfj6VQGPoQCAWUmheppCeJ66YCdSVR1dezR/4MGTZCnT+uZow=="], |
| 495 | 521 | |
| 496 | "@stacksjs/actions/bun-plugin-stx/@stacksjs/stx/@stacksjs/desktop": ["@stacksjs/desktop@0.2.559", "", { "dependencies": { "craft-native": ">=0.0.111" } }, "sha512-0Mt21QCbWiV3EwPGlnCniO03+J3ysY64F8fDf4t/eQilH34B4l75fDCd2SGILHedjphkEd43pyxY4FBj9OQxCQ=="], | |
| 522 | "@stacksjs/ts-svg/@stacksjs/ts-css/bunfig": ["bunfig@0.15.19", "", { "dependencies": { "@stacksjs/clarity": "^0.3.24" }, "bin": { "bunfig": "./dist/cli.js" } }, "sha512-Ig302A74YUmagywXX1Dl8hXNdwjq/MIlB1TBVRduO6kGTMjkFQsxS3LimTFBNEThWypc/JS6cv4+TLeRAl2RGw=="], | |
| 523 | ||
| 524 | "@stacksjs/actions/bun-plugin-stx/@stacksjs/stx/@stacksjs/desktop": ["@stacksjs/desktop@0.2.567", "", { "dependencies": { "craft-native": ">=0.0.111" } }, "sha512-g6X8P8l4A8GW3gNENBl9x7fuN0ZUZZqflRlqQXQ3SGyIWl0rCE/mWxqr7wg5sWT8R4Y30OXMBgz6AIQyILxlIg=="], | |
| 525 | ||
| 526 | "@stacksjs/actions/bun-plugin-stx/@stacksjs/stx/@stacksjs/sanitizer": ["@stacksjs/sanitizer@0.2.567", "", {}, "sha512-gMq1MqsmLTqZ/y173wAi5/7mrfByPFBJH18bDFzoHV/GkcDuy5GwRbpkmNWiuLEBlUArLutautYHRZBiKeyYPg=="], | |
| 527 | ||
| 528 | "@stacksjs/actions/bun-plugin-stx/@stacksjs/stx/bunfig": ["bunfig@0.15.19", "", { "dependencies": { "@stacksjs/clarity": "^0.3.24" }, "bin": { "bunfig": "./dist/cli.js" } }, "sha512-Ig302A74YUmagywXX1Dl8hXNdwjq/MIlB1TBVRduO6kGTMjkFQsxS3LimTFBNEThWypc/JS6cv4+TLeRAl2RGw=="], | |
| 529 | ||
| 530 | "@stacksjs/browser-extension/bun-plugin-stx/@stacksjs/stx/@stacksjs/desktop": ["@stacksjs/desktop@0.2.567", "", { "dependencies": { "craft-native": ">=0.0.111" } }, "sha512-g6X8P8l4A8GW3gNENBl9x7fuN0ZUZZqflRlqQXQ3SGyIWl0rCE/mWxqr7wg5sWT8R4Y30OXMBgz6AIQyILxlIg=="], | |
| 497 | 531 | |
| 498 | "@stacksjs/actions/bun-plugin-stx/@stacksjs/stx/@stacksjs/sanitizer": ["@stacksjs/sanitizer@0.2.559", "", {}, "sha512-9Gg3Qc1eGkI3kmAgPk5IwGI1rIyvXLlGvX7SxwitLbdXu8mScmPPbVWY3KV+1fhCS7xvqZJ1vnRZT4Tk++EoZw=="], | |
| 532 | "@stacksjs/browser-extension/bun-plugin-stx/@stacksjs/stx/@stacksjs/sanitizer": ["@stacksjs/sanitizer@0.2.567", "", {}, "sha512-gMq1MqsmLTqZ/y173wAi5/7mrfByPFBJH18bDFzoHV/GkcDuy5GwRbpkmNWiuLEBlUArLutautYHRZBiKeyYPg=="], | |
| 499 | 533 | |
| 500 | "@stacksjs/browser-extension/bun-plugin-stx/@stacksjs/stx/@stacksjs/desktop": ["@stacksjs/desktop@0.2.559", "", { "dependencies": { "craft-native": ">=0.0.111" } }, "sha512-0Mt21QCbWiV3EwPGlnCniO03+J3ysY64F8fDf4t/eQilH34B4l75fDCd2SGILHedjphkEd43pyxY4FBj9OQxCQ=="], | |
| 534 | "@stacksjs/browser-extension/bun-plugin-stx/@stacksjs/stx/bunfig": ["bunfig@0.15.19", "", { "dependencies": { "@stacksjs/clarity": "^0.3.24" }, "bin": { "bunfig": "./dist/cli.js" } }, "sha512-Ig302A74YUmagywXX1Dl8hXNdwjq/MIlB1TBVRduO6kGTMjkFQsxS3LimTFBNEThWypc/JS6cv4+TLeRAl2RGw=="], | |
| 501 | 535 | |
| 502 | "@stacksjs/browser-extension/bun-plugin-stx/@stacksjs/stx/@stacksjs/sanitizer": ["@stacksjs/sanitizer@0.2.559", "", {}, "sha512-9Gg3Qc1eGkI3kmAgPk5IwGI1rIyvXLlGvX7SxwitLbdXu8mScmPPbVWY3KV+1fhCS7xvqZJ1vnRZT4Tk++EoZw=="], | |
| 536 | "@stacksjs/buddy/bun-plugin-stx/@stacksjs/stx/@stacksjs/desktop": ["@stacksjs/desktop@0.2.567", "", { "dependencies": { "craft-native": ">=0.0.111" } }, "sha512-g6X8P8l4A8GW3gNENBl9x7fuN0ZUZZqflRlqQXQ3SGyIWl0rCE/mWxqr7wg5sWT8R4Y30OXMBgz6AIQyILxlIg=="], | |
| 503 | 537 | |
| 504 | "@stacksjs/buddy/bun-plugin-stx/@stacksjs/stx/@stacksjs/desktop": ["@stacksjs/desktop@0.2.559", "", { "dependencies": { "craft-native": ">=0.0.111" } }, "sha512-0Mt21QCbWiV3EwPGlnCniO03+J3ysY64F8fDf4t/eQilH34B4l75fDCd2SGILHedjphkEd43pyxY4FBj9OQxCQ=="], | |
| 538 | "@stacksjs/buddy/bun-plugin-stx/@stacksjs/stx/@stacksjs/sanitizer": ["@stacksjs/sanitizer@0.2.567", "", {}, "sha512-gMq1MqsmLTqZ/y173wAi5/7mrfByPFBJH18bDFzoHV/GkcDuy5GwRbpkmNWiuLEBlUArLutautYHRZBiKeyYPg=="], | |
| 505 | 539 | |
| 506 | "@stacksjs/buddy/bun-plugin-stx/@stacksjs/stx/@stacksjs/sanitizer": ["@stacksjs/sanitizer@0.2.559", "", {}, "sha512-9Gg3Qc1eGkI3kmAgPk5IwGI1rIyvXLlGvX7SxwitLbdXu8mScmPPbVWY3KV+1fhCS7xvqZJ1vnRZT4Tk++EoZw=="], | |
| 540 | "@stacksjs/buddy/bun-plugin-stx/@stacksjs/stx/bunfig": ["bunfig@0.15.19", "", { "dependencies": { "@stacksjs/clarity": "^0.3.24" }, "bin": { "bunfig": "./dist/cli.js" } }, "sha512-Ig302A74YUmagywXX1Dl8hXNdwjq/MIlB1TBVRduO6kGTMjkFQsxS3LimTFBNEThWypc/JS6cv4+TLeRAl2RGw=="], | |
| 507 | 541 | |
| 508 | 542 | "@stacksjs/components/@stacksjs/stx/@stacksjs/desktop/craft-native": ["craft-native@0.0.173", "", { "bin": { "craft-sdk": "dist/cli.js" } }, "sha512-JzMSiBqX+lSxcpk70flk6gH96Q2Xl4QsG1nisLLvVU/AqqFlYfkY/+MtkdwTfp8t1v1BG4Bt0L/e/O2Rk4APAQ=="], |
| 509 | 543 | } |
| @@ -159,7 +159,7 @@ export default { | ||
| 159 | 159 | urlSchemes: ['wildloop'], |
| 160 | 160 | appIcon: 'public/images/app/wildloop-app-icon.png', |
| 161 | 161 | googleServicesFile: envVars.ANDROID_GOOGLE_SERVICES_FILE, |
| 162 | mapsApiKey: envVars.ANDROID_MAPS_API_KEY, | |
| 162 | mapProvider: 'maplibre', | |
| 163 | 163 | nativeScreens, |
| 164 | 164 | capabilities: { |
| 165 | 165 | backgroundLocation: true, |
| @@ -10,12 +10,12 @@ | ||
| 10 | 10 | "@bughq/stacks": "^0.2.2", |
| 11 | 11 | "@iconify-json/lucide": "^1.2.137", |
| 12 | 12 | "@loghq/stacks": "^0.2.6", |
| 13 | "@stacksjs/composables": "^0.75.219", | |
| 14 | "@stacksjs/mobile": "^0.75.219", | |
| 15 | "@stacksjs/stx": "^0.2.563", | |
| 13 | "@stacksjs/composables": "^0.75.221", | |
| 14 | "@stacksjs/mobile": "^0.75.221", | |
| 15 | "@stacksjs/stx": "^0.2.573", | |
| 16 | 16 | "@ts-analytics/tracking": "^0.1.17", |
| 17 | "craft-native": "^0.0.237", | |
| 18 | "stacks": "^0.75.219", | |
| 17 | "craft-native": "^0.0.244", | |
| 18 | "stacks": "^0.75.221", | |
| 19 | 19 | "ts-health": "^0.1.0", |
| 20 | 20 | "ts-maps": "^0.5.5", |
| 21 | 21 | "ts-qr-codes": "^0.1.8", |
| @@ -73,8 +73,12 @@ export interface NativeApi { | ||
| 73 | 73 | send: (path: string, method?: string, body?: unknown) => Promise<NativeResponse> |
| 74 | 74 | /** Native FormData carries on-device files through Craft's multipart transport. */ |
| 75 | 75 | upload: (path: string, form: FormData) => Promise<NativeResponse> |
| 76 | /** Keep a protected operation with the screen's loaded account; never retarget it. */ | |
| 77 | bindSession: () => Promise<NativeApi> | |
| 76 | 78 | } |
| 77 | 79 | |
| 80 | interface NativeSessionBinding { account: string, bearer: string } | |
| 81 | ||
| 78 | 82 | /** |
| 79 | 83 | * A token reader and sender bound to one screen. |
| 80 | 84 | * |
| @@ -89,6 +93,15 @@ export function createNativeApi(host: NativeApiHost, origin = nativeApiOrigin(ho | ||
| 89 | 93 | let reading: Promise<string> | null = null |
| 90 | 94 | let session = '' |
| 91 | 95 | const sessionKey = (): string => host.snapshots ? JSON.stringify(host.snapshots.get('wildloop-session')) ?? 'null' : '' |
| 96 | const accountChanged = (): Error => new Error('Your account changed. Please try again.') | |
| 97 | const assertSession = async (binding: NativeSessionBinding): Promise<string> => { | |
| 98 | if (binding.account !== sessionKey()) throw accountChanged() | |
| 99 | const current = await host.secureStorage.get('auth_token') | |
| 100 | // The keychain can rotate before the shared account snapshot is published, | |
| 101 | // and cold starts can have a bearer without any snapshot yet. | |
| 102 | if (binding.account !== sessionKey() || current !== binding.bearer) throw accountChanged() | |
| 103 | return binding.bearer | |
| 104 | } | |
| 92 | 105 | const readToken = async (): Promise<string> => { |
| 93 | 106 | const current = sessionKey() |
| 94 | 107 | if (!host.snapshots || session !== current) { |
| @@ -120,11 +133,11 @@ export function createNativeApi(host: NativeApiHost, origin = nativeApiOrigin(ho | ||
| 120 | 133 | return { 'X-CSRF-Token': token, Cookie: `X-CSRF-Token=${token}` } |
| 121 | 134 | } |
| 122 | 135 | |
| 123 | const request = async (path: string, method: string, body: string | FormData | undefined, json: boolean): Promise<NativeResponse> => { | |
| 124 | const account = sessionKey() | |
| 125 | const bearer = await readToken() | |
| 136 | const request = async (path: string, method: string, body: string | FormData | undefined, json: boolean, binding?: NativeSessionBinding): Promise<NativeResponse> => { | |
| 137 | const account = binding?.account ?? sessionKey() | |
| 138 | const bearer = binding ? await assertSession(binding) : await readToken() | |
| 126 | 139 | const csrf = !bearer && !['GET', 'HEAD', 'OPTIONS'].includes(method.toUpperCase()) ? await guestMutationHeaders() : {} |
| 127 | if (account !== sessionKey()) throw new Error('Your account changed. Please try again.') | |
| 140 | if (account !== sessionKey()) throw accountChanged() | |
| 128 | 141 | const response = await fetch(`${origin}${path}`, { |
| 129 | 142 | method, |
| 130 | 143 | headers: { ...nativeHeaders(bearer, json), ...csrf }, |
| @@ -133,14 +146,35 @@ export function createNativeApi(host: NativeApiHost, origin = nativeApiOrigin(ho | ||
| 133 | 146 | // A body that is not JSON is not an exception — an error page is text, and |
| 134 | 147 | // the status is what the screen reads anyway. |
| 135 | 148 | const payload = await response.json().catch(() => null) |
| 136 | if (account !== sessionKey()) throw new Error('Your account changed. Please try again.') | |
| 149 | if (binding) await assertSession(binding) | |
| 150 | else if (account !== sessionKey()) throw accountChanged() | |
| 137 | 151 | return { ok: response.ok, status: response.status, body: payload } |
| 138 | 152 | } |
| 139 | 153 | |
| 154 | const bindSession = async (): Promise<NativeApi> => { | |
| 155 | const account = sessionKey() | |
| 156 | if (reading && session !== account) throw accountChanged() | |
| 157 | // Anchor an already-loaded screen to its last bearer, even on a host | |
| 158 | // without snapshots. Reading a fresh bearer here would retarget its draft. | |
| 159 | const bearer = await (reading ?? readToken()) | |
| 160 | if (account !== sessionKey()) throw accountChanged() | |
| 161 | if (!bearer) throw new Error('Sign in before saving changes.') | |
| 162 | const binding = { account, bearer } | |
| 163 | await assertSession(binding) | |
| 164 | const bound: NativeApi = { | |
| 165 | readToken: () => assertSession(binding), | |
| 166 | send: (path, method = 'GET', body) => request(path, method, body === undefined ? undefined : JSON.stringify(body), body !== undefined, binding), | |
| 167 | upload: (path, form) => request(path, 'POST', form, false, binding), | |
| 168 | bindSession: async () => { await assertSession(binding); return bound }, | |
| 169 | } | |
| 170 | return bound | |
| 171 | } | |
| 172 | ||
| 140 | 173 | return { |
| 141 | 174 | readToken, |
| 142 | 175 | send: (path, method = 'GET', body) => request(path, method, body === undefined ? undefined : JSON.stringify(body), body !== undefined), |
| 143 | 176 | upload: (path, form) => request(path, 'POST', form, false), |
| 177 | bindSession, | |
| 144 | 178 | } |
| 145 | 179 | } |
| 146 | 180 | |
| @@ -0,0 +1,48 @@ | ||
| 1 | import type { NativeStorage } from '@stacksjs/stx/native' | |
| 2 | import type { NativeRecording } from './native-recording' | |
| 3 | ||
| 4 | const KEY = 'wildloop-native-recording-v1' | |
| 5 | const copy = <T>(value: T): T => JSON.parse(JSON.stringify(value)) | |
| 6 | const conflict = () => new Error('The saved recording changed on this device. Pull down to recover it before trying again. Your track has been retained.') | |
| 7 | ||
| 8 | /** Serialize exact native CAS writes; a stale screen never replaces another track. */ | |
| 9 | export function createNativeRecordingCheckpoint(storage: Pick<NativeStorage, 'get' | 'setIfAbsent' | 'compareAndSet'>) { | |
| 10 | let saved: NativeRecording | null = null | |
| 11 | let writes = Promise.resolve() | |
| 12 | function serialize<T>(work: () => Promise<T>): Promise<T> { | |
| 13 | const pending = writes.then(work) | |
| 14 | writes = pending.then(() => {}, () => {}) | |
| 15 | return pending | |
| 16 | } | |
| 17 | function sameRecording(value: NativeRecording): void { | |
| 18 | if (!saved || saved.id !== value.id || saved.userId !== value.userId) throw conflict() | |
| 19 | } | |
| 20 | return { | |
| 21 | read: () => serialize(async () => { | |
| 22 | saved = copy(await storage.get<NativeRecording>(KEY)) | |
| 23 | return copy(saved) | |
| 24 | }), | |
| 25 | register: (value: NativeRecording) => { | |
| 26 | const snapshot = copy(value) | |
| 27 | return serialize(async () => { | |
| 28 | // CAS null updates an existing tombstone; setIfAbsent creates a key. | |
| 29 | // Neither operation may replace an existing account's checkpoint. | |
| 30 | if (!await storage.setIfAbsent(KEY, snapshot) && !await storage.compareAndSet(KEY, null, snapshot)) throw conflict() | |
| 31 | saved = snapshot | |
| 32 | }) | |
| 33 | }, | |
| 34 | save: (value: NativeRecording) => { | |
| 35 | const snapshot = copy(value) | |
| 36 | return serialize(async () => { | |
| 37 | sameRecording(snapshot) | |
| 38 | if (!await storage.compareAndSet(KEY, saved, snapshot)) throw conflict() | |
| 39 | saved = snapshot | |
| 40 | }) | |
| 41 | }, | |
| 42 | clear: (value: NativeRecording) => serialize(async () => { | |
| 43 | sameRecording(value) | |
| 44 | if (!await storage.compareAndSet(KEY, saved, null)) throw conflict() | |
| 45 | saved = null | |
| 46 | }), | |
| 47 | } | |
| 48 | } | |
| @@ -16,6 +16,8 @@ export interface NativeRecording { | ||
| 16 | 16 | targetTerritoryId: number | null |
| 17 | 17 | pauses: Array<{ start: number, end: number | null }> |
| 18 | 18 | samples: RecorderSample[] |
| 19 | phase?: 'starting' | 'recording' | 'stopped' | |
| 20 | endedAt?: number | |
| 19 | 21 | pending?: Record<string, unknown> |
| 20 | 22 | } |
| 21 | 23 | |
| @@ -161,7 +161,7 @@ onMount(() => onNativeAccountChange(craft, () => { rows.set([]); void load() })) | ||
| 161 | 161 | </View> |
| 162 | 162 | |
| 163 | 163 | <View :show="past().length > 0" class="mt-5 gap-2"> |
| 164 | <Text testID="past-heading" class="text-xs font-semibold uppercase text-slate-600">Finished</Text> | |
| 164 | <Text testID="past-heading" class="text-xs font-semibold uppercase text-[#64745f] dark:text-[#91a28b]">Finished</Text> | |
| 165 | 165 | <Pressable :for="row in past()" :key="row.id" testID="past-row" class="rounded-xl bg-white dark:bg-[#1b2c22]/60 p-3" @click="openEvent(row)"> |
| 166 | 166 | <Text class="text-sm font-semibold text-[#253321] dark:text-[#e3ecdf]" :text="row.name" /> |
| 167 | 167 | <Text class="text-xs text-[#64745f] dark:text-[#91a28b]" :text="kind(row) + ' · ' + when(row)" /> |
| @@ -88,8 +88,8 @@ onMount(() => onNativeAccountChange(craft, () => { rows.set([]); kudosed.set({}) | ||
| 88 | 88 | </Pressable> |
| 89 | 89 | <Button :show="activity.trailId" :class="ui.secondary" @click="craft.navigation.open('/trail/' + activity.trailId)" :text="activity.trailName || 'View trail'" /> |
| 90 | 90 | <View class="mt-3 flex-row gap-3"> |
| 91 | <Button testID="feed-kudos" :disabled="pending()[activity.id]" @click="kudos(activity)" :text="(kudosed()[activity.id] ? 'Kudos given' : 'Give kudos') + ' · ' + activity.kudosCount" /> | |
| 92 | <Button @click="craft.navigation.open('/activity/' + activity.id)">Comments</Button> | |
| 91 | <Button testID="feed-kudos" :class="ui.secondary" :disabled="pending()[activity.id]" @click="kudos(activity)" :text="(kudosed()[activity.id] ? 'Kudos given' : 'Give kudos') + ' · ' + activity.kudosCount" /> | |
| 92 | <Button :class="ui.secondary" @click="craft.navigation.open('/activity/' + activity.id)">Comments</Button> | |
| 93 | 93 | </View> |
| 94 | 94 | </View> |
| 95 | 95 | <Button :show="hasMore" :disabled="loading" class="m-4" :class="ui.secondary" @click="load(true)" testID="feed-load-more">Load more</Button> |
| @@ -1,7 +1,8 @@ | ||
| 1 | 1 | <script client lang="ts"> |
| 2 | import { nativeApiOrigin } from '../functions/native-api' | |
| 2 | import { createNativeApi, onNativeAccountChange, type NativeApi } from '../functions/native-api' | |
| 3 | 3 | import type { NativeCraft } from '@stacksjs/stx/native' |
| 4 | import { ACTIVITY_VISIBILITIES, buildManualActivity, MANUAL_ACTIVITY_TYPES, todayLocal } from '../functions/manual-activity' | |
| 4 | import { ACTIVITY_VISIBILITIES, buildManualActivity, MANUAL_ACTIVITY_TYPES, todayLocal, type ManualActivityPayload } from '../functions/manual-activity' | |
| 5 | import { nativeUi as ui } from '../functions/native-ui' | |
| 5 | 6 | |
| 6 | 7 | const craft = (globalThis as typeof globalThis & { craft: NativeCraft }).craft |
| 7 | 8 | const types = [...MANUAL_ACTIVITY_TYPES] |
| @@ -18,6 +19,9 @@ const notes = state('') | ||
| 18 | 19 | const saving = state(false) |
| 19 | 20 | const error = state('') |
| 20 | 21 | let active = true |
| 22 | let accountGeneration = 0 | |
| 23 | let api = createNativeApi(craft) | |
| 24 | let intent: { fields: string, payload: ManualActivityPayload, api: NativeApi } | null = null | |
| 21 | 25 | |
| 22 | 26 | function eventText(event: { text?: unknown }): string { |
| 23 | 27 | return event?.text == null ? '' : String(event.text) |
| @@ -35,18 +39,9 @@ function goBack() { | ||
| 35 | 39 | craft.navigation.back() |
| 36 | 40 | } |
| 37 | 41 | |
| 38 | async function messageFrom(response: Response, fallback: string): Promise<{ body: any, message: string }> { | |
| 39 | const body = await response.json().catch(() => null) | |
| 40 | const fields = body?.fields ?? body?.errors | |
| 41 | const message = fields && typeof fields === 'object' | |
| 42 | ? Object.values(fields).map(String).join(' ') | |
| 43 | : String(body?.message ?? fallback) | |
| 44 | return { body, message } | |
| 45 | } | |
| 46 | ||
| 47 | 42 | async function submit() { |
| 48 | 43 | if (saving()) return |
| 49 | const built = buildManualActivity({ | |
| 44 | const input = { | |
| 50 | 45 | type: types[typeIndex()], |
| 51 | 46 | distance: distance(), |
| 52 | 47 | duration: duration(), |
| @@ -55,58 +50,75 @@ async function submit() { | ||
| 55 | 50 | date: date(), |
| 56 | 51 | notes: notes(), |
| 57 | 52 | visibility: visibilities[visibilityIndex()], |
| 58 | // No id: the token below is the only identity this screen has, and the API | |
| 59 | // takes the actor from the session regardless. Reading one from | |
| 60 | // `route.params` refused a save on a cold start, where `initialScreen` | |
| 61 | // passes no params — with a valid token sitting in secureStorage. | |
| 62 | }, null) | |
| 53 | } | |
| 54 | const built = buildManualActivity(input, null, { uploadId: '' }) | |
| 63 | 55 | if (built.ok === false) { |
| 64 | 56 | error.set(built.error) |
| 65 | 57 | return |
| 66 | 58 | } |
| 67 | 59 | |
| 60 | const generation = accountGeneration | |
| 61 | const current = () => active && generation === accountGeneration | |
| 68 | 62 | saving.set(true) |
| 69 | 63 | error.set('') |
| 70 | 64 | try { |
| 71 | const token = await craft.secureStorage.get('auth_token') | |
| 72 | if (!token) throw new Error('Sign in before saving an activity') | |
| 73 | const response = await fetch(`${nativeApiOrigin(craft)}/api/activities`, { | |
| 74 | method: 'POST', | |
| 75 | headers: { | |
| 76 | Accept: 'application/json', | |
| 77 | Authorization: `Bearer ${token}`, | |
| 78 | 'Content-Type': 'application/json', | |
| 79 | }, | |
| 80 | body: JSON.stringify(built.payload), | |
| 81 | }) | |
| 82 | const result = await messageFrom(response, 'Could not save the activity') | |
| 83 | if (!response.ok) throw new Error(result.message) | |
| 84 | const created = result.body?.activity | |
| 65 | const fields = JSON.stringify(input) | |
| 66 | if (!intent || intent.fields !== fields) { | |
| 67 | const bound = await api.bindSession() | |
| 68 | if (!current()) return | |
| 69 | if (!craft.crypto) throw new Error('Secure activity identities are unavailable in this app build.') | |
| 70 | const id = await craft.crypto.randomUUID() | |
| 71 | if (!current()) return | |
| 72 | await bound.readToken() | |
| 73 | const ready = buildManualActivity(input, null, { uploadId: `manual:${id}` }) | |
| 74 | if (ready.ok === false) throw new Error(ready.error) | |
| 75 | intent = { fields, payload: ready.payload, api: bound } | |
| 76 | } | |
| 77 | const pending = intent | |
| 78 | const response = await pending.api.send('/api/activities', 'POST', pending.payload) | |
| 79 | if (!current()) return | |
| 80 | const result = response.body | |
| 81 | if (!response.ok) { | |
| 82 | const fields = result?.fields ?? result?.errors | |
| 83 | throw new Error(fields && typeof fields === 'object' | |
| 84 | ? Object.values(fields).map(String).join(' ') | |
| 85 | : String(result?.message ?? result?.error ?? 'Could not save the activity')) | |
| 86 | } | |
| 87 | const created = result?.activity | |
| 85 | 88 | if (!created?.id) throw new Error('Wildloop did not return the saved activity') |
| 86 | if (!active) return | |
| 87 | 89 | await craft.haptics.notification('success') |
| 88 | if (!active) return | |
| 90 | if (!current()) return | |
| 89 | 91 | craft.navigation.push('ActivitySaved', { |
| 90 | 92 | id: created.id, |
| 91 | 93 | // The server's answer, not a guess carried in from the route. |
| 92 | 94 | userId: created.userId ?? 0, |
| 93 | activityType: built.payload.activity_type, | |
| 94 | distance: built.payload.distance, | |
| 95 | duration: built.payload.duration, | |
| 96 | pace: built.payload.pace, | |
| 97 | elevation: built.payload.elevation, | |
| 98 | completedAt: built.payload.completed_at, | |
| 95 | activityType: pending.payload.activity_type, | |
| 96 | distance: pending.payload.distance, | |
| 97 | duration: pending.payload.duration, | |
| 98 | pace: pending.payload.pace, | |
| 99 | elevation: pending.payload.elevation, | |
| 100 | completedAt: pending.payload.completed_at, | |
| 99 | 101 | }) |
| 100 | 102 | } |
| 101 | 103 | catch (cause) { |
| 102 | if (active) error.set(cause instanceof Error ? cause.message : 'Could not save the activity') | |
| 104 | if (current()) error.set(cause instanceof Error ? cause.message : 'Could not save the activity') | |
| 103 | 105 | } |
| 104 | 106 | finally { |
| 105 | if (active) saving.set(false) | |
| 107 | if (current()) saving.set(false) | |
| 106 | 108 | } |
| 107 | 109 | } |
| 108 | 110 | |
| 111 | onMount(() => onNativeAccountChange(craft, () => { | |
| 112 | accountGeneration++ | |
| 113 | intent = null | |
| 114 | api = createNativeApi(craft) | |
| 115 | typeIndex.set(0);visibilityIndex.set(1) | |
| 116 | distance.set('');duration.set('');elevation.set('');trailId.set('');notes.set('');date.set(todayLocal()) | |
| 117 | saving.set(false);error.set('') | |
| 118 | void api.readToken().catch(() => {}) | |
| 119 | })) | |
| 109 | 120 | onMount(() => { |
| 121 | void api.readToken().catch(() => {}) | |
| 110 | 122 | craft.navigation.setOptions({ title: 'Log activity', largeTitle: true }) |
| 111 | 123 | }) |
| 112 | 124 | onDestroy(() => { |
| @@ -114,34 +126,34 @@ onDestroy(() => { | ||
| 114 | 126 | }) |
| 115 | 127 | </script> |
| 116 | 128 | |
| 117 | <SafeAreaView class="flex-1 bg-[#f2f4ef] dark:bg-[#101c16]"> | |
| 118 | <ScrollView class="p-4" testID="manual-scroll"> | |
| 119 | <Text class="mt-1 text-sm text-[#51604e] dark:text-[#b0bfac]">The same manual entry saved by the Wildloop web feed.</Text> | |
| 129 | <SafeAreaView :class="ui.screen"> | |
| 130 | <ScrollView class="flex-1 p-4" testID="manual-scroll"> | |
| 131 | <Text class="mt-1" :class="ui.caption">Add a run, hike, walk, or ride to your log.</Text> | |
| 120 | 132 | |
| 121 | 133 | <View class="mt-5 gap-2"> |
| 122 | <Text class="text-sm font-semibold text-slate-200">Activity</Text> | |
| 134 | <Text :class="ui.title">Activity</Text> | |
| 123 | 135 | <Text testID="activity-type" class="text-lg text-[#24724b] dark:text-[#9be579]" :text="types[typeIndex()]" /> |
| 124 | <Button testID="cycle-type" @click="cycleType()">Change activity type</Button> | |
| 136 | <Button testID="cycle-type" :class="ui.secondary" @click="cycleType()">Change activity type</Button> | |
| 125 | 137 | </View> |
| 126 | 138 | |
| 127 | 139 | <View class="mt-4 gap-2"> |
| 128 | <Text class="text-sm text-slate-200">Distance (miles)</Text> | |
| 129 | <TextInput testID="distance-input" placeholder="5.0" :value="distance" @input="distance.set(eventText($event))" /> | |
| 130 | <Text class="text-sm text-slate-200">Duration (MM:SS or H:MM:SS)</Text> | |
| 131 | <TextInput testID="duration-input" placeholder="45:30" :value="duration" @input="duration.set(eventText($event))" /> | |
| 132 | <Text class="text-sm text-slate-200">Elevation gain (feet)</Text> | |
| 133 | <TextInput testID="elevation-input" placeholder="800" :value="elevation" @input="elevation.set(eventText($event))" /> | |
| 134 | <Text class="text-sm text-slate-200">Trail id (optional)</Text> | |
| 135 | <TextInput testID="trail-input" placeholder="42" :value="trailId" @input="trailId.set(eventText($event))" /> | |
| 136 | <Text class="text-sm text-slate-200">Date</Text> | |
| 137 | <TextInput testID="date-input" placeholder="YYYY-MM-DD" :value="date" @input="date.set(eventText($event))" /> | |
| 138 | <Text class="text-sm text-slate-200">Notes</Text> | |
| 139 | <TextInput testID="notes-input" placeholder="How did it go?" :value="notes" @input="notes.set(eventText($event))" /> | |
| 140 | <Text :class="ui.caption">Distance (miles)</Text> | |
| 141 | <TextInput testID="distance-input" :class="ui.input" placeholder="5.0" :value="distance" @input="distance.set(eventText($event))" /> | |
| 142 | <Text :class="ui.caption">Duration (MM:SS or H:MM:SS)</Text> | |
| 143 | <TextInput testID="duration-input" :class="ui.input" placeholder="45:30" :value="duration" @input="duration.set(eventText($event))" /> | |
| 144 | <Text :class="ui.caption">Elevation gain (feet)</Text> | |
| 145 | <TextInput testID="elevation-input" :class="ui.input" placeholder="800" :value="elevation" @input="elevation.set(eventText($event))" /> | |
| 146 | <Text :class="ui.caption">Trail id (optional)</Text> | |
| 147 | <TextInput testID="trail-input" :class="ui.input" placeholder="42" :value="trailId" @input="trailId.set(eventText($event))" /> | |
| 148 | <Text :class="ui.caption">Date</Text> | |
| 149 | <TextInput testID="date-input" :class="ui.input" placeholder="YYYY-MM-DD" :value="date" @input="date.set(eventText($event))" /> | |
| 150 | <Text :class="ui.caption">Notes</Text> | |
| 151 | <TextInput testID="notes-input" :class="ui.input" placeholder="How did it go?" :value="notes" @input="notes.set(eventText($event))" /> | |
| 140 | 152 | </View> |
| 141 | 153 | |
| 142 | 154 | <View class="mt-4 gap-2"> |
| 143 | 155 | <Text testID="visibility" class="text-sm text-[#253321] dark:text-[#e3ecdf]" :text="'Visible to ' + visibilities[visibilityIndex()]" /> |
| 144 | <Button testID="cycle-visibility" @click="cycleVisibility()">Change visibility</Button> | |
| 156 | <Button testID="cycle-visibility" :class="ui.secondary" @click="cycleVisibility()">Change visibility</Button> | |
| 145 | 157 | </View> |
| 146 | 158 | |
| 147 | 159 | <View class="mt-5 gap-2"> |
| @@ -154,7 +166,7 @@ onDestroy(() => { | ||
| 154 | 166 | </View> |
| 155 | 167 | |
| 156 | 168 | <Text :show="error" testID="manual-error" class="mt-4 text-[#b4232a] dark:text-[#ff8487]" :text="error" /> |
| 157 | <Button testID="save-activity" class="mt-5" @click="submit()">Save activity</Button> | |
| 158 | <Button testID="manual-back" class="mt-2" @click="goBack()">Back</Button> | |
| 169 | <Button testID="save-activity" class="mt-5" :class="ui.action" :disabled="saving" :title="saving() ? 'Saving…' : 'Save activity'" @click="submit()" /> | |
| 170 | <Button testID="manual-back" class="mt-2" :class="ui.secondary" @click="goBack()">Back</Button> | |
| 159 | 171 | </ScrollView> |
| 160 | 172 | </SafeAreaView> |
| @@ -5,12 +5,14 @@ import { createHealthWorkout } from '../functions/health-workout' | ||
| 5 | 5 | import { useIntervalFn } from '@stacksjs/composables' |
| 6 | 6 | import { readNativeAccount } from '../functions/native-session' |
| 7 | 7 | import { onNativeAccountChange, createNativeApi } from '../functions/native-api' |
| 8 | import { createNativeRecordingCheckpoint } from '../functions/native-recording-checkpoint' | |
| 8 | 9 | import { nativeOutdoorTiles } from '../functions/native-basemaps' |
| 9 | 10 | import { chooseNativeChoice, nativeChoiceLabel } from '../functions/native-choice' |
| 10 | 11 | import { nativeUi as ui } from '../functions/native-ui' |
| 11 | 12 | import { nativeRecordingDuration, movingSeconds, recordingFigures, recordingPayload, recordingSamples, type NativeRecording } from '../functions/native-recording' |
| 12 | 13 | const craft = (globalThis as typeof globalThis & {craft:NativeCraft}).craft |
| 13 | const KEY = 'wildloop-native-recording-v1' | |
| 14 | const checkpoint = createNativeRecordingCheckpoint(craft.storage) | |
| 15 | let api = createNativeApi(craft) | |
| 14 | 16 | const recording = state<NativeRecording|null>(null) |
| 15 | 17 | const running = state(false) |
| 16 | 18 | const paused = state(false) |
| @@ -23,12 +25,13 @@ const visibility = state('followers') | ||
| 23 | 25 | const gameMode = state('capture') |
| 24 | 26 | const now = state(Date.now()) |
| 25 | 27 | const figures = derived(() => recording() ? recordingFigures(recording()!) : {distance:0,elevation:0}) |
| 26 | const movingTime = derived(() => { const value=recording(); return value ? movingSeconds(value,now()) : 0 }) | |
| 28 | const movingTime = derived(() => { const value=recording(); return value ? movingSeconds(value,value.endedAt ?? (value.pending ? Date.parse(String(value.pending.completed_at)) : now())) : 0 }) | |
| 27 | 29 | const coordinates = derived(() => recording()?.samples.flatMap(sample => [sample.lat,sample.lng]) ?? []) |
| 28 | 30 | let alive = true |
| 29 | 31 | let owner = 0 |
| 30 | 32 | let watch: number | null = null |
| 31 | let writes = Promise.resolve() | |
| 33 | let accountGeneration = 0 | |
| 34 | let recoverQueued = false | |
| 32 | 35 | let recovering = false |
| 33 | 36 | const live=createLiveActivity(craft.liveActivity) |
| 34 | 37 | let lastLiveUpdate=0 |
| @@ -40,17 +43,24 @@ function updateLive(force=false) { | ||
| 40 | 43 | const moving=movingSeconds(value,at) |
| 41 | 44 | void live.update({activityId:'wildloop:'+value.id,title:value.type,symbol:'figure.run',deepLink:'wildloop://record',tint:'#24724b',status:paused()?'Paused':'Recording',paused:paused(),distanceMeters:recordingFigures(value).distance*1609.344,durationSeconds:moving,timerStartedAt:paused()?null:at-moving*1000}).catch(()=>{}) |
| 42 | 45 | } |
| 43 | function persist(value: NativeRecording): Promise<void> { | |
| 44 | const snapshot = JSON.parse(JSON.stringify(value)) | |
| 45 | writes = writes.catch(() => {}).then(async () => {await craft.storage.set(KEY,snapshot)}) | |
| 46 | return writes | |
| 46 | function persist(value: NativeRecording): Promise<void> { return checkpoint.save(value) } | |
| 47 | async function ownedSession(value?: NativeRecording) { | |
| 48 | const bound = await api.bindSession() | |
| 49 | const cached = await readNativeAccount(craft) | |
| 50 | const me = cached ? null : await bound.send('/api/me') | |
| 51 | await bound.readToken() | |
| 52 | const userId = cached?.id ?? (Number(me?.body?.user?.id) || 0) | |
| 53 | if (!userId || (value && value.userId !== userId)) throw new Error('Log in as the account that started this recording before changing or uploading it.') | |
| 54 | return { api: bound, userId } | |
| 47 | 55 | } |
| 48 | async function perform(work:()=>Promise<void>) { | |
| 56 | async function perform(work:(current:()=>boolean)=>Promise<void>) { | |
| 49 | 57 | if (busy()) return |
| 58 | const generation = accountGeneration | |
| 59 | const current = () => alive && generation === accountGeneration | |
| 50 | 60 | busy.set(true);error.set('') |
| 51 | try { await work() } | |
| 52 | catch (cause) { error.set(cause instanceof Error ? cause.message : 'The recording could not be updated. Your track remains on this device.') } | |
| 53 | finally { busy.set(false) } | |
| 61 | try { await work(current) } | |
| 62 | catch (cause) { if (current()) error.set(cause instanceof Error ? cause.message : 'The recording could not be updated. Your track remains on this device.') } | |
| 63 | finally { busy.set(false);if(recoverQueued && alive) void recover() } | |
| 54 | 64 | } |
| 55 | 65 | function append(locations:any[]) { |
| 56 | 66 | const value = recording() |
| @@ -61,29 +71,60 @@ function append(locations:any[]) { | ||
| 61 | 71 | } |
| 62 | 72 | function attachWatch() { |
| 63 | 73 | if (watch !== null || !craft.location) return |
| 74 | const generation=accountGeneration | |
| 75 | const id=recording()?.id | |
| 64 | 76 | watch = craft.location.watchPosition(location => { |
| 65 | if (!alive || busy() || paused() || !running()) return | |
| 77 | if (!alive || generation!==accountGeneration || recording()?.id!==id || busy() || paused() || !running()) return | |
| 66 | 78 | append([location]) |
| 67 | if (recording()) void persist(recording()!).catch(() => error.set('Device storage could not save a checkpoint. Keep the app open until your activity is saved.')) | |
| 79 | if (recording()) void persist(recording()!).catch(() => {if(alive && generation===accountGeneration) error.set('Device storage could not save a checkpoint. Keep the app open until your activity is saved.')}) | |
| 68 | 80 | },{enableHighAccuracy:true}) |
| 69 | 81 | } |
| 70 | 82 | async function recover() { |
| 71 | if (recovering || busy()) return | |
| 83 | if (recovering || busy()) {recoverQueued=true;return} | |
| 84 | recoverQueued=false | |
| 85 | const generation=accountGeneration | |
| 86 | const current=()=>alive && generation===accountGeneration | |
| 72 | 87 | recovering=true;loading.set(true) |
| 73 | 88 | try { |
| 74 | const api=createNativeApi(craft) | |
| 75 | const cached=await readNativeAccount(craft) | |
| 76 | const me=cached ? null : await api.send('/api/me') | |
| 77 | if (!alive) return | |
| 78 | owner=cached?.id ?? (Number(me?.body?.user?.id)||0) | |
| 79 | const saved=await craft.storage.get<NativeRecording>(KEY) | |
| 80 | const host=await craft.location?.getRecordingState() | |
| 81 | if (!alive) return | |
| 89 | const token=await api.readToken() | |
| 90 | const session=token ? await ownedSession() : null | |
| 91 | if (!current()) return | |
| 92 | owner=session?.userId ?? 0 | |
| 93 | const saved=await checkpoint.read() | |
| 94 | if (!current()) return | |
| 82 | 95 | if (saved && saved.userId !== owner) { |
| 83 | 96 | recording.set(null);running.set(false) |
| 84 | 97 | error.set('Log in as the account that started the saved recording before resuming or uploading it.') |
| 85 | 98 | return |
| 86 | 99 | } |
| 100 | if (saved && !saved.id) { | |
| 101 | recording.set(saved);running.set(false);paused.set(false) | |
| 102 | error.set('This saved track has no verifiable native recording identity. Its checkpoint has been retained; it cannot be attached to another recording.') | |
| 103 | return | |
| 104 | } | |
| 105 | // A finished checkpoint is sufficient for an offline retry. Do not read | |
| 106 | // the shared GPS slot, which may now belong to another recording. | |
| 107 | if (saved?.pending) { | |
| 108 | recording.set(saved);running.set(false);paused.set(false) | |
| 109 | notice.set('Your finished recording is saved on this device. Retry the upload when connected.') | |
| 110 | return | |
| 111 | } | |
| 112 | if (!owner) {recording.set(null);running.set(false);paused.set(false);return} | |
| 113 | await session!.api.readToken() | |
| 114 | let host | |
| 115 | try { host=await craft.location?.getRecordingState(saved?.id ? {expectedId:saved.id} : undefined) } | |
| 116 | catch (cause) { | |
| 117 | // A registered intent may not have reached the host. An explicit native | |
| 118 | // ownership conflict permits retrying that same ID, never a replacement. | |
| 119 | if (saved?.phase==='starting' && cause instanceof Error && 'code' in cause && cause.code==='RECORDING_CONFLICT') { | |
| 120 | if (!current()) return | |
| 121 | recording.set(saved);running.set(false);paused.set(false) | |
| 122 | notice.set('The recording start was interrupted. Retry starting with its saved identity, or discard the intent.') | |
| 123 | return | |
| 124 | } | |
| 125 | throw cause | |
| 126 | } | |
| 127 | if (!current()) return | |
| 87 | 128 | if (host?.active && (!saved || (saved.id && saved.id !== host.id))) { |
| 88 | 129 | error.set('A device recording is already active. Its account checkpoint could not be verified, so it has been retained without starting another recording.') |
| 89 | 130 | return |
| @@ -91,66 +132,91 @@ async function recover() { | ||
| 91 | 132 | recording.set(saved) |
| 92 | 133 | running.set(Boolean(host?.active)) |
| 93 | 134 | paused.set(Boolean(host?.paused)) |
| 94 | if (saved && host?.active && !saved.pending) { | |
| 95 | append(await craft.location!.readRecording()) | |
| 135 | if (saved && host?.id===saved.id && !saved.pending) { | |
| 136 | const fixes=await craft.location!.readRecording({expectedId:saved.id!}) | |
| 137 | if (!current()) return | |
| 138 | recording.set({...saved,phase:host.active ? 'recording' : 'stopped',startedAt:host.startedAt ?? saved.startedAt,...host.active ? {} : {endedAt:saved.endedAt ?? Date.now()}}) | |
| 139 | append(fixes) | |
| 96 | 140 | await persist(recording()!) |
| 97 | attachWatch() | |
| 141 | if (!current()) return | |
| 142 | if(host.active) attachWatch() | |
| 98 | 143 | notice.set('Your recording has been recovered from this device.');updateLive(true) |
| 99 | 144 | } |
| 100 | 145 | if (saved?.pending) notice.set('Your finished recording is saved on this device. Retry the upload when connected.') |
| 101 | 146 | now.set(Date.now()) |
| 102 | 147 | } |
| 103 | catch { if (alive) error.set('Could not check the device recording. Pull down to retry before starting another activity.') } | |
| 104 | finally { recovering=false;if(alive) loading.set(false) } | |
| 148 | catch (cause) { if (current()) error.set(cause instanceof Error ? cause.message : 'Could not check the device recording. Pull down to retry before starting another activity.') } | |
| 149 | finally { recovering=false;if(current()) loading.set(false);if(recoverQueued && alive) void recover() } | |
| 105 | 150 | } |
| 106 | function start() { void perform(async () => { | |
| 107 | if (!craft.location) throw new Error('Location recording is unavailable in this app build.') | |
| 108 | const api=createNativeApi(craft) | |
| 109 | const cached=await readNativeAccount(craft) | |
| 110 | const me=cached ? null : await api.send('/api/me') | |
| 111 | const userId=cached?.id ?? (Number(me?.body?.user?.id)||0) | |
| 112 | if (!userId) {craft.navigation.open('/login?next=/record');return} | |
| 113 | if (recording()) throw new Error('Save your previous recording before starting another.') | |
| 114 | if ((await craft.location.getRecordingState()).active) throw new Error('A recording is already active on this device. Pull down to recover it.') | |
| 115 | const value:NativeRecording={userId,id:null,startedAt:Date.now(),type:activityType(),visibility:visibility(),gameMode:gameMode() === 'capture' ? 'capture':'free',targetTerritoryId:Number(craft.route.params.target)||null,pauses:[],samples:[]} | |
| 116 | // Bind the account before location starts. The host owns background fixes; | |
| 117 | // this checkpoint owns the account and upload identity across app restarts. | |
| 151 | function start() { void perform(async current => { | |
| 152 | if (!craft.location || !craft.crypto) throw new Error('Secure location recording is unavailable in this app build.') | |
| 153 | const previous=recording() | |
| 154 | const session=await ownedSession(previous ?? undefined) | |
| 155 | if (!current()) return | |
| 156 | if (previous && previous.phase!=='starting') throw new Error('Save your previous recording before starting another.') | |
| 157 | const host=await craft.location.getRecordingState() | |
| 158 | if (!current()) return | |
| 159 | let value=previous | |
| 160 | if (!value) { | |
| 161 | if (host.active) throw new Error('A recording is already active on this device. Pull down to recover it.') | |
| 162 | const id=await craft.crypto.randomUUID() | |
| 163 | if (!current()) return | |
| 164 | await session.api.readToken() | |
| 165 | value={userId:session.userId,id,phase:'starting',startedAt:Date.now(),type:activityType(),visibility:visibility(),gameMode:gameMode() === 'capture' ? 'capture':'free',targetTerritoryId:Number(craft.route.params.target)||null,pauses:[],samples:[]} | |
| 166 | await checkpoint.register(value) | |
| 167 | if (!current()) return | |
| 168 | recording.set(value) | |
| 169 | } | |
| 170 | // A retry can attach only to the same registered start. A different active | |
| 171 | // native recording is retained and never stopped or replaced here. | |
| 172 | if (host.active && host.id!==value.id) throw new Error('Another recording is active on this device. Your saved intent has been retained.') | |
| 173 | await session.api.readToken() | |
| 174 | const started=host.active ? await craft.location.getRecordingState({expectedId:value.id!}) : await craft.location.startRecording({enableHighAccuracy:true,recordingId:value.id!}) | |
| 175 | if (!current()) return | |
| 176 | if (!started.active || started.id!==value.id) throw new Error('Location recording did not start with its saved identity. Your checkpoint has been retained.') | |
| 177 | value={...value,phase:'recording',startedAt:started.startedAt ?? value.startedAt} | |
| 118 | 178 | await persist(value) |
| 119 | recording.set(value) | |
| 120 | const host=await craft.location.startRecording({enableHighAccuracy:true}) | |
| 121 | if (!host.active || !host.id) throw new Error('Location recording did not start. Check location permission and try again.') | |
| 122 | recording.set({...value,id:host.id,startedAt:host.startedAt ?? value.startedAt}) | |
| 123 | owner=value.userId | |
| 124 | await persist(recording()!) | |
| 125 | running.set(true);paused.set(false);now.set(Date.now());notice.set('Recording continues with your phone locked.') | |
| 126 | attachWatch() | |
| 127 | updateLive(true) | |
| 179 | if (!current()) return | |
| 180 | recording.set(value);owner=value.userId | |
| 181 | running.set(true);paused.set(Boolean(started.paused));now.set(Date.now());notice.set('Recording continues with your phone locked.') | |
| 182 | attachWatch();updateLive(true) | |
| 128 | 183 | await craft.setKeepAwake?.(true) |
| 129 | 184 | }) } |
| 130 | function togglePause() { void perform(async () => { | |
| 185 | function togglePause() { void perform(async current => { | |
| 131 | 186 | const value=recording() |
| 132 | if (!value || !running() || !craft.location) return | |
| 187 | if (!value?.id || !running() || !craft.location) return | |
| 188 | const session=await ownedSession(value) | |
| 189 | if (!current()) return | |
| 190 | await session.api.readToken() | |
| 133 | 191 | const at=Date.now() |
| 192 | let next=value | |
| 134 | 193 | if (paused()) { |
| 135 | await craft.location.resumeRecording() | |
| 136 | const pauses=value.pauses.map((pause,index) => index === value.pauses.length-1 && pause.end === null ? {...pause,end:at}:pause) | |
| 137 | recording.set({...value,pauses});paused.set(false) | |
| 194 | await craft.location.resumeRecording({expectedId:value.id}) | |
| 195 | if (!current()) return | |
| 196 | next={...value,pauses:value.pauses.map((pause,index) => index===value.pauses.length-1 && pause.end===null ? {...pause,end:at}:pause)} | |
| 197 | paused.set(false) | |
| 138 | 198 | } |
| 139 | 199 | else { |
| 140 | await craft.location.pauseRecording() | |
| 141 | append(await craft.location.readRecording()) | |
| 142 | recording.set({...recording()!,pauses:[...value.pauses,{start:at,end:null}]});paused.set(true) | |
| 200 | await craft.location.pauseRecording({expectedId:value.id}) | |
| 201 | const fixes=await craft.location.readRecording({expectedId:value.id}) | |
| 202 | if (!current()) return | |
| 203 | next={...value,samples:recordingSamples(value,fixes),pauses:[...value.pauses,{start:at,end:null}]} | |
| 204 | paused.set(true) | |
| 143 | 205 | } |
| 144 | await persist(recording()!);now.set(at);updateLive(true) | |
| 206 | await persist(next) | |
| 207 | if (!current()) return | |
| 208 | recording.set(next);now.set(at);updateLive(true) | |
| 145 | 209 | }) } |
| 146 | async function upload() { | |
| 210 | async function upload(current:()=>boolean) { | |
| 147 | 211 | const value=recording() |
| 148 | 212 | if (!value?.pending) return |
| 149 | const api=createNativeApi(craft) | |
| 213 | const {api}=await ownedSession(value) | |
| 214 | if (!current()) return | |
| 150 | 215 | const me=await api.send('/api/me') |
| 151 | 216 | if (!me.ok || Number(me.body?.user?.id) !== value.userId) throw new Error('Log in as the account that started this recording before uploading it.') |
| 152 | 217 | const response=await api.send('/api/activities','POST',value.pending) |
| 153 | 218 | if (!response.ok || !response.body?.activity?.id) throw new Error(response.body?.error || 'Upload failed. Your finished track is saved on this device; retry when connected.') |
| 219 | if (!current()) return | |
| 154 | 220 | const activity=response.body.activity |
| 155 | 221 | let result='Activity saved.' |
| 156 | 222 | if (value.gameMode === 'capture' && activity.captureEligible) { |
| @@ -165,34 +231,68 @@ async function upload() { | ||
| 165 | 231 | catch {result+=' Your workout could not be exported to Health; check its permission in Settings.'} |
| 166 | 232 | } |
| 167 | 233 | await craft.snapshots.set('wildloop-activity-notice',{activityId:activity.id,message:result}) |
| 168 | await writes.catch(()=>{}) | |
| 169 | await craft.storage.remove(KEY) | |
| 234 | if (!current()) return | |
| 235 | if (value.id && value.phase==='stopped') await craft.location!.acknowledgeRecording({expectedId:value.id}) | |
| 236 | await checkpoint.clear(value) | |
| 237 | if (!current()) return | |
| 170 | 238 | recording.set(null);notice.set(result) |
| 171 | 239 | craft.navigation.open(`/activity/${activity.id}`) |
| 172 | 240 | } |
| 173 | function finish() { void perform(async () => { | |
| 174 | if (!recording() || !craft.location) return | |
| 241 | function finish() { void perform(async current => { | |
| 242 | let value=recording() | |
| 243 | if (!value?.id || !craft.location) throw new Error('The native recording identity is unavailable. Your checkpoint has been retained.') | |
| 244 | const session=await ownedSession(value) | |
| 245 | if (!current()) return | |
| 246 | await session.api.readToken() | |
| 175 | 247 | if (running()) { |
| 176 | const ended=await craft.location.stopRecording() | |
| 177 | append(ended.locations) | |
| 178 | running.set(false);paused.set(false) | |
| 248 | const ended=await craft.location.stopRecording({expectedId:value.id}) | |
| 249 | if (!current()) return | |
| 250 | const endedAt=Date.now() | |
| 251 | value={...value,samples:recordingSamples(value,ended.locations),phase:'stopped',endedAt} | |
| 252 | await persist(value) | |
| 253 | if (!current()) return | |
| 254 | recording.set(value);running.set(false);paused.set(false) | |
| 179 | 255 | await live.end().catch(()=>{}) |
| 180 | 256 | await craft.setKeepAwake?.(false) |
| 257 | if (!current()) return | |
| 258 | } | |
| 259 | if (value.phase==='starting') throw new Error('Retry starting this recording before finishing it.') | |
| 260 | if (value.samples.length < 2) throw new Error('Not enough GPS fixes to save an activity. Your available fixes remain on this device.') | |
| 261 | if (!value.pending) { | |
| 262 | value={...value,pending:recordingPayload(value,value.endedAt ?? Date.now())} | |
| 263 | await persist(value) | |
| 264 | if (!current()) return | |
| 265 | recording.set(value) | |
| 181 | 266 | } |
| 182 | const value=recording()! | |
| 183 | if (value.samples.length < 2) {await persist(value);throw new Error('Not enough GPS fixes to save an activity. Your available fixes remain on this device.')} | |
| 184 | if (!value.id) throw new Error('The native recording identity is unavailable. Your fixes have been retained.') | |
| 185 | if (!value.pending) recording.set({...value,pending:recordingPayload(value,Date.now())}) | |
| 186 | await persist(recording()!) | |
| 187 | await upload() | |
| 267 | await upload(current) | |
| 188 | 268 | }) } |
| 189 | 269 | function retry() {void perform(upload)} |
| 190 | function discard() {void perform(async()=>{ | |
| 270 | function discard() {void perform(async current=>{ | |
| 271 | const value=recording() | |
| 272 | if (!value) return | |
| 191 | 273 | if (running()) throw new Error('Finish your recording before discarding it.') |
| 274 | const session=await ownedSession(value) | |
| 275 | if (!current()) return | |
| 192 | 276 | if (!await craft.dialog?.confirm({title:'Discard this recording?',message:'This removes the unsaved track from this device.',confirmLabel:'Discard',destructive:true})) return |
| 193 | await writes.catch(()=>{});await craft.storage.remove(KEY);recording.set(null);notice.set('Recording discarded.') | |
| 277 | if (!current()) return | |
| 278 | await session.api.readToken() | |
| 279 | // A saved start may have reached native GPS before its acknowledgement was | |
| 280 | // delivered. Never clear its owner checkpoint while it could still be active. | |
| 281 | if (value.id && !value.pending && value.phase!=='starting') { | |
| 282 | const host=await craft.location!.getRecordingState({expectedId:value.id}) | |
| 283 | if (host.active) throw new Error('This recording is still active. Pull down to recover and finish it before discarding.') | |
| 284 | } | |
| 285 | if (value.phase==='starting') { | |
| 286 | const host=await craft.location!.getRecordingState() | |
| 287 | if (host.active && host.id===value.id) throw new Error('This recording started on the device. Retry starting to recover it before finishing.') | |
| 288 | } | |
| 289 | if (!current()) return | |
| 290 | if (value.id && value.phase==='stopped') await craft.location!.acknowledgeRecording({expectedId:value.id}) | |
| 291 | await checkpoint.clear(value) | |
| 292 | if (!current()) return | |
| 293 | recording.set(null);notice.set('Recording discarded.') | |
| 194 | 294 | })} |
| 195 | onMount(()=>onNativeAccountChange(craft,()=>{recording.set(null);running.set(false);paused.set(false);owner=0;if(watch!==null){craft.location?.clearWatch(watch);watch=null}void recover()})) | |
| 295 | onMount(()=>onNativeAccountChange(craft,()=>{accountGeneration++;api=createNativeApi(craft);recoverQueued=true;loading.set(true);recording.set(null);running.set(false);paused.set(false);notice.set('');error.set('');owner=0;if(watch!==null){craft.location?.clearWatch(watch);watch=null}void recover()})) | |
| 196 | 296 | onMount(()=>{ |
| 197 | 297 | craft.navigation.setOptions({title:'Record',largeTitle:false}) |
| 198 | 298 | void recover() |
| @@ -230,7 +330,8 @@ onMount(()=>{ | ||
| 230 | 330 | <Button testID="record-finish" :disabled="busy" :class="ui.action" class="flex-1" @click="finish()" title="Finish and save" /> |
| 231 | 331 | </View> |
| 232 | 332 | <Button :show="recording()?.pending" testID="record-retry" :disabled="busy" :class="ui.action" @click="retry()" title="Retry upload" /> |
| 233 | <Button :show="recording() && !running() && !recording()?.pending" :disabled="busy" @click="finish()">Save retained recording</Button> | |
| 333 | <Button :show="recording()?.phase==='starting' && !running()" testID="record-retry-start" :disabled="busy || loading" :class="ui.action" @click="start()">Retry starting</Button> | |
| 334 | <Button :show="recording() && !running() && !recording()?.pending && recording()?.phase!=='starting'" :disabled="busy" @click="finish()">Save retained recording</Button> | |
| 234 | 335 | <Button :show="recording() && !running()" :disabled="busy" :class="ui.danger" @click="discard()">Discard recording</Button> |
| 235 | 336 | </View> |
| 236 | 337 | </View> |
| @@ -226,7 +226,7 @@ onMount(() => onNativeAccountChange(craft, () => { rows.set([]); void load() })) | ||
| 226 | 226 | <Text class="text-sm font-semibold text-[#192414] dark:text-[#f0f5ec]" :text="holderName(holder)" /> |
| 227 | 227 | <Text testID="holder-zones" class="text-xs text-[#64745f] dark:text-[#91a28b]" :text="holderZones(holder)" /> |
| 228 | 228 | </View> |
| 229 | <Text testID="holder-area" class="text-sm font-semibold text-slate-200" :text="holderArea(holder)" /> | |
| 229 | <Text testID="holder-area" class="text-sm font-semibold text-[#192414] dark:text-[#f0f5ec]" :text="holderArea(holder)" /> | |
| 230 | 230 | </View> |
| 231 | 231 | </View> |
| 232 | 232 | </View> |
| @@ -1,5 +1,6 @@ | ||
| 1 | 1 | <script client lang="ts"> |
| 2 | 2 | import type { NativeCraft } from '@stacksjs/stx/native' |
| 3 | import type { NativeApi } from '../functions/native-api' | |
| 3 | 4 | import { onNativeAccountChange, createNativeApi, loadOutcome, nativeApiOrigin } from '../functions/native-api' |
| 4 | 5 | import { nativeBasemap, nativeBasemapOptions } from '../functions/native-basemaps' |
| 5 | 6 | import { chooseNativeChoice, nativeChoiceLabel } from '../functions/native-choice' |
| @@ -58,6 +59,7 @@ const formError = state('') | ||
| 58 | 59 | const photoDrafts = state<Array<{file:any,id?:string}>>([]) |
| 59 | 60 | let alive = true |
| 60 | 61 | let generation = 0 |
| 62 | let accountGeneration = 0 | |
| 61 | 63 | |
| 62 | 64 | const surfaces = derived(() => trailSurfaceTypes(rawTrail()?.surface)) |
| 63 | 65 | const routeAvailable = derived(() => parts().some(part => part.length >= 4)) |
| @@ -66,12 +68,10 @@ const mapRegion = derived(() => region() ?? nativeRouteRegion(parts(), {lat:trai | ||
| 66 | 68 | const photos = derived(() => trailGallery(trail(),reviews())) |
| 67 | 69 | |
| 68 | 70 | function textOf(event: any): string { return String(event?.text ?? '') } |
| 71 | function accountIsCurrent(version:number): boolean { return alive && version === accountGeneration } | |
| 69 | 72 | function profilePath(): string { return trailProfilePath(profile()) } |
| 70 | 73 | function photoUrls(row: any): string[] { return parsePhotoList(row.photos).map(url=>url.startsWith('/') ? nativeApiOrigin(craft)+url : url) } |
| 71 | 74 | function photoUrl(url:string):string {return url.startsWith('/') ? nativeApiOrigin(craft)+url : url} |
| 72 | function weatherLine(day: any, index: number): string { | |
| 73 | return `${forecastDayLabel(day.date, index)}: ${day.label}, ${Math.round(day.low)} to ${Math.round(day.high)}°F` | |
| 74 | } | |
| 75 | 75 | function fitRoute() { |
| 76 | 76 | region.set(null) |
| 77 | 77 | inspected.set(null) |
| @@ -122,15 +122,20 @@ function signedIn(): boolean { | ||
| 122 | 122 | } |
| 123 | 123 | async function toggleSaved() { |
| 124 | 124 | if (loading() || !viewerReady() || busy() || !signedIn()) return |
| 125 | const current = accountGeneration | |
| 126 | const method = saved() ? 'DELETE' : 'PUT' | |
| 125 | 127 | busy.set(true) |
| 126 | 128 | try { |
| 127 | const response = await api.send(`/api/trails/${wanted}/save`, saved() ? 'DELETE' : 'PUT') | |
| 129 | const operation = await api.bindSession() | |
| 130 | if (!accountIsCurrent(current)) return | |
| 131 | const response = await operation.send(`/api/trails/${wanted}/save`, method) | |
| 132 | if (!accountIsCurrent(current)) return | |
| 128 | 133 | if (!response.ok) throw new Error(response.body?.error || 'The trail could not be saved.') |
| 129 | 134 | saved.set(Boolean(response.body.saved)) |
| 130 | 135 | notice.set(saved() ? 'Trail saved.' : 'Trail removed from saved trails.') |
| 131 | 136 | } |
| 132 | catch (cause) { notice.set(cause instanceof Error ? cause.message : 'The trail could not be saved.') } | |
| 133 | finally { busy.set(false) } | |
| 137 | catch (cause) { if (accountIsCurrent(current)) notice.set(cause instanceof Error ? cause.message : 'The trail could not be saved.') } | |
| 138 | finally { if (accountIsCurrent(current)) busy.set(false) } | |
| 134 | 139 | } |
| 135 | 140 | function editReview() { |
| 136 | 141 | if (loading() || !viewerReady() || busy() || !signedIn()) return |
| @@ -144,73 +149,85 @@ function editReview() { | ||
| 144 | 149 | photoDrafts.set([]) |
| 145 | 150 | composer.set(true) |
| 146 | 151 | } |
| 147 | async function reloadReviews() { | |
| 152 | async function reloadReviews(operation: NativeApi) { | |
| 148 | 153 | const current = generation |
| 149 | 154 | viewerReady.set(false) |
| 150 | const [response, personal] = await Promise.all([api.send(`/api/trails/${wanted}/reviews`), api.send(`/api/trails/${wanted}/viewer`)]) | |
| 155 | const [response, personal] = await Promise.all([operation.send(`/api/trails/${wanted}/reviews`), operation.send(`/api/trails/${wanted}/viewer`)]) | |
| 151 | 156 | if (!alive || current !== generation) return |
| 152 | 157 | if (personal.ok) { ownReview.set(personal.body.review ?? null); viewerReady.set(true) } |
| 153 | 158 | if (response.ok) reviews.set(response.body.reviews ?? []) |
| 154 | 159 | if (!response.ok || !personal.ok) throw new Error('Your change was saved, but reviews could not refresh. Pull down to retry.') |
| 155 | 160 | } |
| 156 | 161 | async function pickReviewPhoto() { |
| 157 | if (busy()) return | |
| 162 | if (loading() || !viewerReady() || busy() || !signedIn()) return | |
| 163 | const current = accountGeneration | |
| 158 | 164 | if (photoDrafts().length + photoUrls(ownReview() ?? {}).length >= 10) { formError.set('A review can have up to 10 photos.'); return } |
| 159 | 165 | try { |
| 160 | 166 | if (!craft.files) throw new Error('Photo picking is unavailable in this app build.') |
| 161 | 167 | const file = await craft.files.pickImage({source:'library',maxPixels:2400,quality:0.88}) |
| 168 | if (!accountIsCurrent(current)) return | |
| 162 | 169 | if (file) photoDrafts.set([...photoDrafts(),{file}]) |
| 163 | 170 | } |
| 164 | catch (cause) { formError.set(cause instanceof Error ? cause.message : 'A photo could not be selected.') } | |
| 171 | catch (cause) { if (accountIsCurrent(current)) formError.set(cause instanceof Error ? cause.message : 'A photo could not be selected.') } | |
| 165 | 172 | } |
| 166 | 173 | async function submitReview() { |
| 167 | if (busy()) return | |
| 174 | if (loading() || !viewerReady() || busy() || !signedIn()) return | |
| 168 | 175 | const problem = reviewFormError(rating(), content()) |
| 169 | 176 | if (problem) { formError.set(problem); return } |
| 177 | const current = accountGeneration | |
| 170 | 178 | busy.set(true) |
| 171 | 179 | formError.set('') |
| 172 | 180 | try { |
| 181 | const operation = await api.bindSession() | |
| 182 | if (!accountIsCurrent(current)) return | |
| 173 | 183 | for (let index=0;index<photoDrafts().length;index++) { |
| 174 | 184 | const draft=photoDrafts()[index] |
| 175 | 185 | if (draft.id) continue |
| 176 | 186 | const form=new FormData() |
| 177 | 187 | form.append('photo',draft.file) |
| 178 | const response=await api.upload(`/api/trails/${wanted}/photos`,form) | |
| 188 | const response=await operation.upload(`/api/trails/${wanted}/photos`,form) | |
| 189 | if (!accountIsCurrent(current)) return | |
| 179 | 190 | const body=response.body |
| 180 | 191 | if (!response.ok || !body.photo?.id) throw new Error(body.error || 'A photo could not be uploaded. Try saving again.') |
| 181 | 192 | photoDrafts.set(photoDrafts().map((entry,at)=>at===index ? {...entry,id:body.photo.id}:entry)) |
| 182 | 193 | } |
| 183 | const response = await api.send(`/api/trails/${wanted}/reviews`, 'POST', { | |
| 194 | const response = await operation.send(`/api/trails/${wanted}/reviews`, 'POST', { | |
| 184 | 195 | rating: rating(), content: content().trim(), conditions: conditions() || null, |
| 185 | 196 | difficulty: difficulty() || null, visit_date: visitDate() || null, |
| 186 | 197 | ...(photoDrafts().length ? {photo_ids:photoDrafts().map(draft=>draft.id),retain_photos:true}:{}), |
| 187 | 198 | }) |
| 199 | if (!accountIsCurrent(current)) return | |
| 188 | 200 | if (!response.ok) throw new Error(Object.values(response.body?.fields ?? {}).join('. ') || response.body?.error || 'The review could not be saved.') |
| 189 | 201 | if (response.body.trail) trail.set({...trail(),...response.body.trail}) |
| 190 | 202 | composer.set(false) |
| 191 | 203 | notice.set('Your review has been saved.') |
| 192 | try { await reloadReviews() } catch (cause) { notice.set(cause instanceof Error ? cause.message : 'Pull down to refresh reviews.') } | |
| 204 | try { await reloadReviews(operation) } catch (cause) { if (accountIsCurrent(current)) notice.set(cause instanceof Error ? cause.message : 'Pull down to refresh reviews.') } | |
| 193 | 205 | } |
| 194 | catch (cause) { formError.set(cause instanceof Error ? cause.message : 'The review could not be saved.') } | |
| 195 | finally { busy.set(false) } | |
| 206 | catch (cause) { if (accountIsCurrent(current)) formError.set(cause instanceof Error ? cause.message : 'The review could not be saved.') } | |
| 207 | finally { if (accountIsCurrent(current)) busy.set(false) } | |
| 196 | 208 | } |
| 197 | 209 | async function deleteReview() { |
| 198 | 210 | if (loading() || !viewerReady() || !ownReview() || busy()) return |
| 199 | const confirmed = await craft.dialog?.confirm({ title: 'Delete your review?', message: 'Your rating, review, and condition report will be removed from this trail.', confirmLabel: 'Delete review', destructive: true }) | |
| 200 | if (!confirmed) return | |
| 211 | const current = accountGeneration | |
| 201 | 212 | busy.set(true) |
| 202 | 213 | try { |
| 203 | const response = await api.send(`/api/trails/${wanted}/reviews`, 'DELETE') | |
| 214 | const operation = await api.bindSession() | |
| 215 | if (!accountIsCurrent(current)) return | |
| 216 | const confirmed = await craft.dialog?.confirm({ title: 'Delete your review?', message: 'Your rating, review, and condition report will be removed from this trail.', confirmLabel: 'Delete review', destructive: true }) | |
| 217 | if (!confirmed || !accountIsCurrent(current)) return | |
| 218 | const response = await operation.send(`/api/trails/${wanted}/reviews`, 'DELETE') | |
| 219 | if (!accountIsCurrent(current)) return | |
| 204 | 220 | if (!response.ok) throw new Error(response.body?.error || 'The review could not be deleted.') |
| 205 | 221 | if (response.body.trail) trail.set({...trail(),...response.body.trail}) |
| 206 | 222 | ownReview.set(null) |
| 207 | 223 | notice.set('Your review has been deleted.') |
| 208 | try { await reloadReviews() } catch (cause) { notice.set(cause instanceof Error ? cause.message : 'Pull down to refresh reviews.') } | |
| 224 | try { await reloadReviews(operation) } catch (cause) { if (accountIsCurrent(current)) notice.set(cause instanceof Error ? cause.message : 'Pull down to refresh reviews.') } | |
| 209 | 225 | } |
| 210 | catch (cause) { notice.set(cause instanceof Error ? cause.message : 'The review could not be deleted.') } | |
| 211 | finally { busy.set(false) } | |
| 226 | catch (cause) { if (accountIsCurrent(current)) notice.set(cause instanceof Error ? cause.message : 'The review could not be deleted.') } | |
| 227 | finally { if (accountIsCurrent(current)) busy.set(false) } | |
| 212 | 228 | } |
| 213 | 229 | async function load() { |
| 230 | if (busy()) return | |
| 214 | 231 | const current = ++generation |
| 215 | 232 | loading.set(true) |
| 216 | 233 | viewerReady.set(false) |
| @@ -259,7 +276,14 @@ onMount(() => { | ||
| 259 | 276 | const stop = craft.navigation.onAppear(() => { void load() }) |
| 260 | 277 | return () => { alive = false; generation++; stop() } |
| 261 | 278 | }) |
| 262 | onMount(() => onNativeAccountChange(craft, () => { viewer.set(0); saved.set(false); ownReview.set(null); viewerReady.set(false); reviews.set([]); composer.set(false); void load() })) | |
| 279 | onMount(() => onNativeAccountChange(craft, () => { | |
| 280 | accountGeneration++ | |
| 281 | busy.set(false) | |
| 282 | viewer.set(0); saved.set(false); ownReview.set(null); viewerReady.set(false); reviews.set([]) | |
| 283 | composer.set(false); rating.set(0); content.set(''); conditions.set(''); difficulty.set(''); visitDate.set('') | |
| 284 | photoDrafts.set([]); formError.set(''); notice.set('') | |
| 285 | void load() | |
| 286 | })) | |
| 263 | 287 | </script> |
| 264 | 288 | |
| 265 | 289 | <View :class="ui.screen"> |
| @@ -290,11 +314,11 @@ onMount(() => onNativeAccountChange(craft, () => { viewer.set(0); saved.set(fals | ||
| 290 | 314 | <Map testID="trail-native-map" class="absolute inset-0 h-full w-full" :region="routeAvailable() ? region() : mapRegion()" :fitToBounds="region() === null && routeAvailable()" :edgePadding="{top:32,right:32,bottom:profileOpen() ? 220 : 88,left:32}" :mapType="basemap().mapType" :tileOverlay="basemap().tileOverlay" :scrollEnabled="false" :zoomEnabled="false" :rotateEnabled="false" :showsCompass="true" :showsScale="true" :showsUserLocation="showLocation" :cameraPitch="pitch" :cameraHeading="heading" @region-change-complete="camera($event)"> |
| 291 | 315 | <MapPolyline :for="(part, index) in parts()" :key="'route-outline-' + index" :id="'route-outline-' + index" :geometry="part" :strokeColor="trailRouteStyle.casing" :strokeWidth="9" /> |
| 292 | 316 | <MapPolyline :for="(part, index) in parts()" :key="index" :id="'route-' + index" :geometry="part" :strokeColor="trailRouteStyle.line" :strokeWidth="5" /> |
| 293 | <MapMarker :for="(sign,index) in directionSigns()" :key="index" :id="'direction-' + index" :geometry="sign.point" :bearing="sign.bearing" title="Recorded route direction" /> | |
| 294 | <MapMarker id="trailhead" title="Trailhead" :geometry="[trailhead()?.lat, trailhead()?.lng]" /> | |
| 295 | <MapMarker :show="inspected()" id="elevation-point" title="Elevation position" :geometry="[inspected()?.lat, inspected()?.lng]" /> | |
| 317 | <MapMarker :for="(sign,index) in directionSigns()" :key="index" :id="'direction-' + index" :geometry="sign.point" :bearing="sign.bearing" :pinColor="trailRouteStyle.casing" title="Recorded route direction" /> | |
| 318 | <MapMarker id="trailhead" title="Trailhead" pinColor="#24724b" :geometry="[trailhead()?.lat, trailhead()?.lng]" /> | |
| 319 | <MapMarker :show="inspected()" id="elevation-point" title="Elevation position" pinColor="#176bad" :geometry="[inspected()?.lat, inspected()?.lng]" /> | |
| 296 | 320 | </Map> |
| 297 | <Button testID="trail-map-preview-expand" accessibilityLabel="Open interactive map" :class="ui.floating" class="absolute top-3 right-3" @click="craft.navigation.open('/trail/' + wanted + '/map')" title="Expand map" /> | |
| 321 | <Button testID="trail-map-fullscreen" accessibilityLabel="Expand map" :class="ui.floating" class="absolute top-3 right-3" @click="craft.navigation.open('/trail/' + wanted + '/map')" title="Expand map" /> | |
| 298 | 322 | <View class="absolute bottom-5 left-3 right-3 rounded-[20px] bg-white p-3 shadow-lg dark:bg-[#1b2c22]" testID="trail-floating-profile"> |
| 299 | 323 | <View class="flex-row items-center justify-between"><Text :class="ui.caption">Elevation profile</Text><Button testID="trail-profile-toggle" class="h-11 min-w-[44px] text-[15px] font-semibold text-[#253321] dark:text-[#e3ecdf]" accessibilityLabel="Toggle elevation profile" :accessibilityValue="profileOpen() ? 'Elevation profile expanded' : 'Elevation profile collapsed'" @click="profileOpen.set(!profileOpen())" :text="profileOpen() ? 'Hide' : 'Show'" /></View> |
| 300 | 324 | <View :show="profileOpen"> |
| @@ -307,7 +331,7 @@ onMount(() => onNativeAccountChange(craft, () => { viewer.set(0); saved.set(fals | ||
| 307 | 331 | </View> |
| 308 | 332 | <Text :show="!profile().some(point => point.ft !== null)" :class="ui.caption">Elevation data is unavailable for this route.</Text> |
| 309 | 333 | <Text :show="profile().some(point => point.ft !== null)" :text="profileFigures().miles.toFixed(1) + ' mi · ' + Math.round(profileFigures().low) + '–' + Math.round(profileFigures().high) + ' ft'" :class="ui.caption" /> |
| 310 | <Slider :show="profile().length > 1" testID="trail-elevation-slider" accessibilityLabel="Explore elevation and position along the route" :value="profilePosition" :minimumValue="0" :maximumValue="profile().length - 1" :step="1" @change="inspect($event)" /> | |
| 334 | <Slider :show="profile().length > 1" testID="trail-elevation-slider" accessibilityLabel="Explore elevation and position along the route" :value="profilePosition" :minimumValue="0" :maximumValue="Math.max(0,profile().length - 1)" :step="1" @change="inspect($event)" /> | |
| 311 | 335 | <Text :show="inspected()" :text="inspected()?.mi.toFixed(2) + ' mi, ' + (inspected()?.ft === null ? 'elevation unavailable' : inspected()?.ft + ' ft')" :class="ui.caption" /> |
| 312 | 336 | </View> |
| 313 | 337 | </View> |
| @@ -321,8 +345,7 @@ onMount(() => onNativeAccountChange(craft, () => { viewer.set(0); saved.set(fals | ||
| 321 | 345 | <View class="mt-2 flex-row flex-wrap gap-2"> |
| 322 | 346 | <Button testID="trail-map-3d" :class="ui.secondary" @click="pitch.set(pitch() > 0 ? 0 : 45)" :text="pitch() > 0 ? '2D' : '3D'" /> |
| 323 | 347 | <Button :class="ui.secondary" @click="heading.set(0)">North</Button> |
| 324 | <Button testID="trail-map-locate" :class="ui.secondary" @click="locate()">My location</Button> | |
| 325 | <Button testID="trail-map-fullscreen" :class="ui.secondary" @click="craft.navigation.open('/trail/' + wanted + '/map')">Expand map</Button> | |
| 348 | <Button testID="trail-map-locate" :class="ui.secondary" accessibilityLabel="My location" :accessibilityValue="showLocation() ? 'Your location is shown' : ''" @click="locate()">My location</Button> | |
| 326 | 349 | </View> |
| 327 | 350 | <Button testID="trail-map-export" :disabled="!routeAvailable()" :class="ui.secondary" class="mt-2" @click="exportRoute()">Export GPX</Button> |
| 328 | 351 | <Button testID="trail-map-layer" :class="ui.secondary" :title="'Map layer' + ': ' + nativeChoiceLabel(nativeBasemapOptions,mapType())" accessibilityLabel="Map layer" :accessibilityValue="nativeChoiceLabel(nativeBasemapOptions,mapType())" @click="chooseNativeChoice(craft,'Map layer',nativeBasemapOptions,choice=>{mapType.set(choice)},message=>error.set(message))" /> |
| @@ -330,15 +353,30 @@ onMount(() => onNativeAccountChange(craft, () => { viewer.set(0); saved.set(fals | ||
| 330 | 353 | </View> |
| 331 | 354 | <Text :class="ui.header">Conditions</Text> |
| 332 | 355 | <View :class="ui.card"> |
| 333 | <Text :class="ui.title">Weather</Text> | |
| 356 | <Text testID="trail-weather-heading" :class="ui.title">Weather</Text> | |
| 334 | 357 | <Text :show="!forecast()" :class="ui.caption">No forecast is available right now. Pull down to retry.</Text> |
| 335 | 358 | <View :show="forecast()" class="gap-3 mt-2"> |
| 336 | <Text :show="forecast()?.current" :text="Math.round(forecast()?.current.temperature) + '°F, ' + forecast()?.current.label" :class="ui.body" /> | |
| 337 | <Text :for="(day, index) in forecast()?.days ?? []" :key="day.date" :text="weatherLine(day, index)" :class="ui.body" /> | |
| 359 | <View :show="forecast()?.current" class="gap-1 py-2"> | |
| 360 | <Text :text="Math.round(forecast()?.current.temperature) + '°F'" class="text-[42px] font-semibold text-[#192414] dark:text-[#f0f5ec]" /> | |
| 361 | <Text :text="forecast()?.current.label" :class="ui.body" /> | |
| 362 | </View> | |
| 363 | <View :for="(day, index) in forecast()?.days ?? []" :key="day.date" class="gap-1 py-3 border-t border-[#e7edde] dark:border-[#314536]"> | |
| 364 | <View class="flex-row items-center justify-between gap-3"> | |
| 365 | <Text :text="forecastDayLabel(day.date, index)" :class="ui.title" /> | |
| 366 | <Text :text="Math.round(day.low) + '°–' + Math.round(day.high) + '°F'" :class="ui.body" /> | |
| 367 | </View> | |
| 368 | <Text :text="day.label" :class="ui.caption" /> | |
| 369 | <Text :show="Number.isFinite(day.windMph) && Number.isFinite(day.precipitationMm)" :text="Math.round(day.windMph) + ' mph wind · ' + (day.precipitationMm / 25.4).toFixed(2) + ' in precipitation'" :class="ui.caption" /> | |
| 370 | </View> | |
| 338 | 371 | <Button @click="craft.linking.openURL(forecast()?.attribution.url)" :class="ui.caption" :text="forecast()?.attribution.label" /> |
| 339 | 372 | </View> |
| 340 | <Text class="mt-5" :class="ui.title">Surface types</Text> | |
| 341 | <Text :for="surface in surfaces()" :key="surface.label" :text="surface.label" :class="ui.body" /> | |
| 373 | </View> | |
| 374 | <View :class="ui.card" class="mt-3 gap-2"> | |
| 375 | <Text testID="trail-surfaces-heading" :class="ui.title">Surface types</Text> | |
| 376 | <View :for="surface in surfaces()" :key="surface.label" :class="ui.row"> | |
| 377 | <View class="h-5 w-5 rounded-[4px]" :style="{backgroundColor:surface.color}" /> | |
| 378 | <Text :text="surface.label" :class="ui.body" /> | |
| 379 | </View> | |
| 342 | 380 | <Text :show="!surfaces().length" :class="ui.caption">Surface information has not been recorded for this trail.</Text> |
| 343 | 381 | </View> |
| 344 | 382 | <Text :class="ui.header">Reviews</Text> |
| @@ -102,9 +102,9 @@ onMount(() => { void load(); return () => { alive = false } }) | ||
| 102 | 102 | @region-change-complete="camera($event)"> |
| 103 | 103 | <MapPolyline :for="(part,index) in parts()" :key="'route-outline-' + index" :id="'route-outline-' + index" :geometry="part" :strokeColor="trailRouteStyle.casing" :strokeWidth="9" /> |
| 104 | 104 | <MapPolyline :for="(part,index) in parts()" :key="index" :id="'route-' + index" :geometry="part" :strokeColor="trailRouteStyle.line" :strokeWidth="5" /> |
| 105 | <MapMarker :for="(sign,index) in signs()" :key="index" :id="'direction-' + index" :geometry="sign.point" :bearing="sign.bearing" title="Recorded route direction" /> | |
| 106 | <MapMarker id="trailhead" title="Trailhead" :geometry="[trailhead()?.lat, trailhead()?.lng]" /> | |
| 107 | <MapMarker :show="inspected()" id="elevation-point" title="Elevation position" :geometry="[inspected()?.lat, inspected()?.lng]" /> | |
| 105 | <MapMarker :for="(sign,index) in signs()" :key="index" :id="'direction-' + index" :geometry="sign.point" :bearing="sign.bearing" :pinColor="trailRouteStyle.casing" title="Recorded route direction" /> | |
| 106 | <MapMarker id="trailhead" title="Trailhead" pinColor="#24724b" :geometry="[trailhead()?.lat, trailhead()?.lng]" /> | |
| 107 | <MapMarker :show="inspected()" id="elevation-point" title="Elevation position" pinColor="#176bad" :geometry="[inspected()?.lat, inspected()?.lng]" /> | |
| 108 | 108 | </Map> |
| 109 | 109 | <View class="absolute top-3 left-3 right-3 flex-row flex-wrap gap-2"> |
| 110 | 110 | <Button testID="map-layer" class="bg-white dark:bg-[#1b2c22] rounded-[16px]" :class="ui.secondary" :title="'Map layer' + ': ' + nativeChoiceLabel(nativeBasemapOptions,mapType())" accessibilityLabel="Map layer" :accessibilityValue="nativeChoiceLabel(nativeBasemapOptions,mapType())" @click="chooseNativeChoice(craft,'Map layer',nativeBasemapOptions,choice=>{mapType.set(choice)},message=>error.set(message))" /> |
| @@ -138,7 +138,7 @@ onMount(() => { void load(); return () => { alive = false } }) | ||
| 138 | 138 | <Button testID="map-fit" class="flex-1" :class="ui.secondary" @click="fit()">Fit route</Button> |
| 139 | 139 | <Button testID="map-zoom-in" class="w-11" :class="ui.secondary" @click="region.set(nativeZoomRegion(currentRegion(),0.5))" accessibilityLabel="Zoom in">+</Button> |
| 140 | 140 | <Button testID="map-zoom-out" class="w-11" :class="ui.secondary" @click="region.set(nativeZoomRegion(currentRegion(),2))" accessibilityLabel="Zoom out">−</Button> |
| 141 | <Button testID="map-locate" class="flex-1" :class="ui.secondary" :accessibilityValue="showLocation() ? 'Your location is shown' : ''" @click="locate()">My location</Button> | |
| 141 | <Button testID="map-locate" class="flex-1" :class="ui.secondary" accessibilityLabel="My location" :accessibilityValue="showLocation() ? 'Your location is shown' : ''" @click="locate()">My location</Button> | |
| 142 | 142 | </View> |
| 143 | 143 | <View class="flex-row gap-2"> |
| 144 | 144 | <Button testID="map-export" class="flex-1" :class="ui.secondary" @click="exportRoute()">Export GPX</Button> |
| @@ -175,13 +175,13 @@ onMount(() => onNativeAccountChange(craft, () => { rows.set([]); mine.set([]); v | ||
| 175 | 175 | @input="query.set(eventText($event))" |
| 176 | 176 | /> |
| 177 | 177 | <View class="flex-row gap-2"> |
| 178 | <Button testID="trails-submit" @click="search()">Search</Button> | |
| 179 | <Button :show="query() !== ''" testID="trails-clear" @click="clear()">Clear</Button> | |
| 178 | <Button testID="trails-submit" :class="ui.action + ' flex-1'" @click="search()">Search</Button> | |
| 179 | <Button :show="query() !== ''" testID="trails-clear" :class="ui.secondary" @click="clear()">Clear</Button> | |
| 180 | 180 | </View> |
| 181 | 181 | </View> |
| 182 | 182 | |
| 183 | 183 | <View :show="listMode() === 'all'" class="mt-3 gap-3"> |
| 184 | <View class="flex-row gap-3"><Button testID="trails-near-me" @click="locate()">Near me</Button><Button @click="showFilters.set(!showFilters())">Filters</Button><Button :show="nearby()" @click="nearby.set(null);load()">Anywhere</Button></View> | |
| 184 | <View class="flex-row gap-3"><Button testID="trails-near-me" :class="ui.secondary" @click="locate()">Near me</Button><Button :class="ui.secondary" @click="showFilters.set(!showFilters())">Filters</Button><Button :show="nearby()" :class="ui.secondary" @click="nearby.set(null);load()">Anywhere</Button></View> | |
| 185 | 185 | <View :show="showFilters" class="gap-3 rounded-xl bg-white dark:bg-[#1b2c22] p-4"> |
| 186 | 186 | <Text class="text-[#51604e] dark:text-[#b0bfac]">Difficulty</Text><Button testID="trails-difficulty" :class="ui.secondary" :title="'Difficulty' + ': ' + nativeChoiceLabel([{label:'Any difficulty',value:'all'},{label:'Easy',value:'easy'},{label:'Moderate',value:'moderate'},{label:'Hard',value:'hard'}],difficulty())" accessibilityLabel="Difficulty" :accessibilityValue="nativeChoiceLabel([{label:'Any difficulty',value:'all'},{label:'Easy',value:'easy'},{label:'Moderate',value:'moderate'},{label:'Hard',value:'hard'}],difficulty())" @click="chooseNativeChoice(craft,'Difficulty',[{label:'Any difficulty',value:'all'},{label:'Easy',value:'easy'},{label:'Moderate',value:'moderate'},{label:'Hard',value:'hard'}],choice=>{difficulty.set(choice);load()},message=>error.set(message))" /> |
| 187 | 187 | <Text class="text-[#51604e] dark:text-[#b0bfac]">Route type</Text><Button :class="ui.secondary" :title="'Route type' + ': ' + nativeChoiceLabel([{label:'Any route',value:'all'},{label:'Loop',value:'loop'},{label:'Out and back',value:'out-and-back'},{label:'Point to point',value:'point-to-point'}],routeType())" accessibilityLabel="Route type" :accessibilityValue="nativeChoiceLabel([{label:'Any route',value:'all'},{label:'Loop',value:'loop'},{label:'Out and back',value:'out-and-back'},{label:'Point to point',value:'point-to-point'}],routeType())" @click="chooseNativeChoice(craft,'Route type',[{label:'Any route',value:'all'},{label:'Loop',value:'loop'},{label:'Out and back',value:'out-and-back'},{label:'Point to point',value:'point-to-point'}],choice=>{routeType.set(choice);load()},message=>error.set(message))" /> |
| @@ -1,8 +1,6 @@ | ||
| 1 | 1 | import process from 'node:process' |
| 2 | import mobile from '../config/mobile' | |
| 3 | 2 | |
| 4 | 3 | const platform=process.argv[2] |
| 5 | 4 | if(platform!=='ios' && platform!=='android')throw new Error('Usage: bun scripts/build-mobile.ts ios|android') |
| 6 | if(platform==='android' && !mobile.android.mapsApiKey?.trim())throw new Error('Set ANDROID_MAPS_API_KEY to a Google Maps SDK for Android key before building Wildloop. Native trail maps require this configured SDK credential.') | |
| 7 | 5 | const build=Bun.spawn(['./buddy',`build:${platform}`,...process.argv.slice(3)],{stdout:'inherit',stderr:'inherit',stdin:'inherit'}) |
| 8 | 6 | process.exitCode=await build.exited |
| @@ -98,10 +98,9 @@ export function selectAndroidDeepLinkActivity(output: string, packageName: strin | ||
| 98 | 98 | return null |
| 99 | 99 | } |
| 100 | 100 | |
| 101 | export function selectAndroidHomePackage(output: string): string | null { | |
| 102 | const component = output.split(/\s+/).find(value => value.includes('/')) | |
| 103 | const packageName = component?.split('/')[0] | |
| 104 | return packageName || null | |
| 101 | export function assertAndroidPlatformHealthy(events: string): void { | |
| 102 | if (/am_(anr|crash)/.test(events)) | |
| 103 | throw new Error('Android platform crash/ANR invalidates native acceptance. Inspect the emulator diagnostics.') | |
| 105 | 104 | } |
| 106 | 105 | |
| 107 | 106 | export function selectIosSimulator(payload: SimctlDevices): IosDevice | null { |
| @@ -114,10 +113,11 @@ export function selectIosSimulator(payload: SimctlDevices): IosDevice | null { | ||
| 114 | 113 | return candidates.find(device => device.state === 'Booted') ?? candidates[0] ?? null |
| 115 | 114 | } |
| 116 | 115 | |
| 117 | export function maestroReportSummary(xml: string): { failures: number, tests: number } { | |
| 116 | export function maestroReportSummary(xml: string): { failures: number, tests: number, skipped: number } { | |
| 118 | 117 | return { |
| 119 | 118 | failures: xml.match(/<(?:failure|error)\b/g)?.length ?? 0, |
| 120 | 119 | tests: xml.match(/<testcase\b/g)?.length ?? 0, |
| 120 | skipped: xml.match(/<skipped\b/g)?.length ?? 0, | |
| 121 | 121 | } |
| 122 | 122 | } |
| 123 | 123 | |
| @@ -288,6 +288,7 @@ function runMaestroFlow( | ||
| 288 | 288 | const summary = maestroReportSummary(readFileSync(report, 'utf8')) |
| 289 | 289 | if (summary.tests === 0) throw new Error(`Maestro ran no ${platform} tests`) |
| 290 | 290 | if (summary.failures > 0) throw new Error(`Maestro reported ${summary.failures} failed ${platform} test(s)`) |
| 291 | if (summary.skipped > 0) throw new Error(`Maestro skipped ${summary.skipped} required ${platform} test(s)`) | |
| 291 | 292 | } |
| 292 | 293 | |
| 293 | 294 | function runMaestroJourneys(platform: MobilePlatform, deviceId: string, signedInURL: string | null): void { |
| @@ -348,19 +349,10 @@ function seedTestLocation(platform: MobilePlatform, deviceId: string): void { | ||
| 348 | 349 | ]) |
| 349 | 350 | } |
| 350 | 351 | |
| 351 | /** | |
| 352 | * Google APIs images occasionally leave Pixel Launcher in an ANR state while | |
| 353 | * Wildloop is foregrounded. The launcher is not part of this journey, so stop | |
| 354 | * that background process before Maestro launches the app under test. | |
| 355 | */ | |
| 356 | function stopAndroidHomeLauncher(deviceId: string): void { | |
| 357 | const home = execute([ | |
| 358 | 'adb', '-s', deviceId, 'shell', 'cmd', 'package', 'resolve-activity', '--brief', | |
| 359 | '-a', 'android.intent.action.MAIN', '-c', 'android.intent.category.HOME', | |
| 360 | ], { capture: true }) | |
| 361 | const packageName = selectAndroidHomePackage(home) | |
| 362 | if (packageName) | |
| 363 | execute(['adb', '-s', deviceId, 'shell', 'am', 'force-stop', packageName]) | |
| 352 | function checkAndroidPlatform(deviceId: string): void { | |
| 353 | assertAndroidPlatformHealthy(execute([ | |
| 354 | 'adb', '-s', deviceId, 'logcat', '-b', 'events', '-d', '-v', 'threadtime', | |
| 355 | ], { capture: true })) | |
| 364 | 356 | } |
| 365 | 357 | |
| 366 | 358 | function appId(platform: MobilePlatform): string { |
| @@ -372,21 +364,23 @@ function appId(platform: MobilePlatform): string { | ||
| 372 | 364 | function runAndroid(preview: boolean): void { |
| 373 | 365 | requireCommand('adb') |
| 374 | 366 | const devices = parseAdbDevices(execute(['adb', 'devices'], { capture: true })) |
| 375 | if (devices.length !== 1) throw new Error(`Expected exactly one ready Android device, found ${devices.length}`) | |
| 367 | const device = process.env.ANDROID_SERIAL ?? (devices.length === 1 ? devices[0] : undefined) | |
| 368 | if (!device || !devices.includes(device)) throw new Error('Select a ready Android device with ANDROID_SERIAL, or connect exactly one device.') | |
| 376 | 369 | |
| 377 | 370 | const apk = process.env.MOBILE_E2E_APP |
| 378 | 371 | ?? join(generatedRoot, 'android/app/build/outputs/apk/debug/app-debug.apk') |
| 379 | 372 | requirePath(apk, 'Android E2E APK') |
| 380 | execute(['adb', '-s', devices[0], 'install', '-r', apk]) | |
| 373 | execute(['adb', '-s', device, 'install', '-r', apk]) | |
| 381 | 374 | if (preview) { |
| 382 | execute(['adb', '-s', devices[0], 'shell', 'am', 'force-stop', appId('android')]) | |
| 383 | execute(['adb', '-s', devices[0], 'shell', 'monkey', '-p', appId('android'), '-c', 'android.intent.category.LAUNCHER', '1']) | |
| 384 | console.log(`Wildloop is open on Android device ${devices[0]}.`) | |
| 375 | execute(['adb', '-s', device, 'shell', 'am', 'force-stop', appId('android')]) | |
| 376 | execute(['adb', '-s', device, 'shell', 'monkey', '-p', appId('android'), '-c', 'android.intent.category.LAUNCHER', '1']) | |
| 377 | console.log(`Wildloop is open on Android device ${device}.`) | |
| 385 | 378 | } |
| 386 | 379 | else { |
| 387 | stopAndroidHomeLauncher(devices[0]) | |
| 388 | seedTestLocation('android', devices[0]) | |
| 389 | runMaestroJourneys('android', devices[0], null) | |
| 380 | checkAndroidPlatform(device) | |
| 381 | seedTestLocation('android', device) | |
| 382 | runMaestroJourneys('android', device, null) | |
| 383 | checkAndroidPlatform(device) | |
| 390 | 384 | } |
| 391 | 385 | } |
| 392 | 386 | |
| @@ -0,0 +1,166 @@ | ||
| 1 | import { mkdir, readFile, writeFile } from 'node:fs/promises' | |
| 2 | import { join, resolve } from 'node:path' | |
| 3 | import process from 'node:process' | |
| 4 | import mobile from '../config/mobile' | |
| 5 | import { QA_PORTS } from '../tests/browser/qa-ports' | |
| 6 | import { QA_SOCIAL_ACCOUNT } from '../tests/browser/qa-social-account' | |
| 7 | import { decodeRouteParts, primaryRoutePart } from '../resources/functions/trail-geometry' | |
| 8 | import { assertAndroidPlatformHealthy, maestroEnvironment, maestroReportSummary, parseAdbDevices, validateGeneratedNativeApp } from './run-mobile-e2e' | |
| 9 | ||
| 10 | interface PersistedReview { id: number, rating: number, content: string } | |
| 11 | ||
| 12 | /** Check server persistence independently of the native screen's local state. */ | |
| 13 | export function assertNativeReviewPersistence( | |
| 14 | reviews: PersistedReview[], phase: 'create' | 'edit' | 'delete', content: string, previousId?: number, | |
| 15 | ): number | undefined { | |
| 16 | if (phase === 'delete') { | |
| 17 | if (reviews.length) throw new Error('The native delete did not remove the persisted review.') | |
| 18 | return | |
| 19 | } | |
| 20 | const rating = phase === 'create' ? 5 : 4 | |
| 21 | if (reviews.length !== 1 || reviews[0].content !== content || reviews[0].rating !== rating) | |
| 22 | throw new Error(`The native ${phase} did not persist the expected review and rating.`) | |
| 23 | if (phase === 'edit' && reviews[0].id !== previousId) | |
| 24 | throw new Error('Editing created a second review instead of updating the original.') | |
| 25 | return reviews[0].id | |
| 26 | } | |
| 27 | ||
| 28 | /** Use only an owned emulator, a disposable database and normally published SDKs. */ | |
| 29 | export async function runNativeTrailAndroidE2e(): Promise<void> { | |
| 30 | const root = resolve('storage/framework/runtime/e2e/native-trail-android') | |
| 31 | const project = resolve('storage/framework/mobile/android') | |
| 32 | const origin = `http://127.0.0.1:${QA_PORTS.app}` | |
| 33 | await mkdir(root, { recursive: true }) | |
| 34 | async function command(args: string[], name: string, env: Record<string, string> = {}): Promise<string> { | |
| 35 | console.log(`Native Android trail QA: ${name}`) | |
| 36 | const child = Bun.spawn(args, { env: { ...process.env, ...env }, stdout: 'pipe', stderr: 'pipe' }) | |
| 37 | const [stdout, stderr, status] = await Promise.all([ | |
| 38 | new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited, | |
| 39 | ]) | |
| 40 | await writeFile(join(root, `${name}.log`), stdout + stderr) | |
| 41 | if (status) throw new Error(`${args[0]} exited ${status}; see ${name}.log`) | |
| 42 | return stdout | |
| 43 | } | |
| 44 | if (!process.argv.includes('--skip-build')) { | |
| 45 | await command(['bun', 'run', 'build:android'], 'app-build', { MOBILE_URL: `${origin}/feed` }) | |
| 46 | } | |
| 47 | validateGeneratedNativeApp(project) | |
| 48 | const config = JSON.parse(await readFile(join(project, 'craft.config.json'), 'utf8')) | |
| 49 | if (config.devServerURL !== `${origin}/feed` || config.mapProvider !== 'maplibre') | |
| 50 | throw new Error('Android trail QA requires its own loopback API and the native MapLibre provider.') | |
| 51 | const gradle = await readFile(join(project, 'app/build.gradle.kts'), 'utf8') | |
| 52 | if (!gradle.includes('org.maplibre.gl:android-sdk:') || gradle.includes('play-services-maps')) | |
| 53 | throw new Error('Android trail QA requires the real key-free native map SDK.') | |
| 54 | if (process.argv.includes('--build-only')) return | |
| 55 | ||
| 56 | const devices = parseAdbDevices(await command(['adb', 'devices'], 'devices')) | |
| 57 | const device = process.env.NATIVE_QA_ANDROID_DEVICE | |
| 58 | if (!device || !/^emulator-\d+$/.test(device) || !devices.includes(device)) throw new Error('NATIVE_QA_ANDROID_DEVICE must name an owned, ready emulator.') | |
| 59 | const adb = (args: string[], name: string) => command(['adb', '-s', device, ...args], name) | |
| 60 | const platformEvents = async (phase: string) => { | |
| 61 | const events = await adb(['logcat', '-b', 'events', '-d', '-v', 'threadtime'], `platform-${phase}`) | |
| 62 | assertAndroidPlatformHealthy(events) | |
| 63 | } | |
| 64 | await platformEvents('before') | |
| 65 | for (const port of [QA_PORTS.app, QA_PORTS.api]) { | |
| 66 | const occupied = await fetch(`http://127.0.0.1:${port}/api/health`, { signal: AbortSignal.timeout(1000) }).then(() => true).catch(() => false) | |
| 67 | if (occupied) throw new Error(`QA port ${port} is occupied; select a different QA_PORT_BASE.`) | |
| 68 | } | |
| 69 | const qa = Bun.spawn(['bun', 'scripts/start-recording-qa.ts'], { stdout: Bun.file(join(root, 'qa-server.log')), stderr: 'inherit' }) | |
| 70 | const forwards: number[] = [] | |
| 71 | try { | |
| 72 | const deadline = Date.now() + 180000 | |
| 73 | while (!await fetch(`${origin}/api/health`, { signal: AbortSignal.timeout(1000) }).then(r => r.ok).catch(() => false)) { | |
| 74 | if (qa.exitCode !== null || Date.now() > deadline) throw new Error('Isolated native QA server did not become ready.') | |
| 75 | await Bun.sleep(250) | |
| 76 | } | |
| 77 | for (const port of [QA_PORTS.app, QA_PORTS.api]) { | |
| 78 | await adb(['reverse', `tcp:${port}`, `tcp:${port}`], `reverse-${port}`) | |
| 79 | forwards.push(port) | |
| 80 | } | |
| 81 | const listed = await (await fetch(`${origin}/api/trails?q=Craggy&country=all&limit=10`)).json() as any | |
| 82 | let trail: any | |
| 83 | for (const candidate of listed.trails ?? []) { | |
| 84 | const detail = await (await fetch(`${origin}/api/trails/${candidate.id}`)).json() as any | |
| 85 | if (detail.trail?.hasGeometry) { trail = detail.trail; break } | |
| 86 | } | |
| 87 | if (!trail) throw new Error('Native Android QA requires its seeded recorded route.') | |
| 88 | const profileResponse = await fetch(`${origin}/api/trails/${trail.id}/profile`) | |
| 89 | if (!profileResponse.ok) throw new Error('Native Android QA requires the trail profile endpoint to be available.') | |
| 90 | const profile = (await profileResponse.json()) as { points: Array<{ mi: number, ft: number | null }> } | |
| 91 | const endpoint = profile.points?.at(-1) | |
| 92 | if (!endpoint || profile.points.length < 2 || !Number.isFinite(endpoint.mi) || (endpoint.ft !== null && !Number.isFinite(endpoint.ft))) | |
| 93 | throw new Error('Native Android QA requires a selectable recorded route profile.') | |
| 94 | const endElevation = endpoint.ft === null ? 'elevation unavailable' : `${endpoint.ft} ft` | |
| 95 | const first = primaryRoutePart([...decodeRouteParts(trail.geometry), ...(Array.isArray(trail.pieceRoutes) ? trail.pieceRoutes : [])])[0] | |
| 96 | if (!first) throw new Error('The seeded trail has no recorded route start.') | |
| 97 | await adb(['emu', 'geo', 'fix', String(first[1]), String(first[0])], 'seed-location') | |
| 98 | const reviews = async (): Promise<PersistedReview[]> => { | |
| 99 | const response = await fetch(`${origin}/api/trails/${trail.id}/reviews`, { cache: 'no-store' }) | |
| 100 | if (!response.ok) throw new Error(`Reading persisted reviews failed: ${response.status}`) | |
| 101 | return ((await response.json()) as any).reviews | |
| 102 | } | |
| 103 | if ((await reviews()).length) throw new Error('The isolated trail must start without reviews.') | |
| 104 | await adb(['install', '-r', join(project, 'app/build/outputs/apk/debug/app-debug.apk')], 'app-install') | |
| 105 | const marker = crypto.randomUUID().slice(0, 8) | |
| 106 | const review = `Android QA ${marker}: clear signs and a beautiful ridge.` | |
| 107 | const edit = `Android QA ${marker}: updated after a return visit.` | |
| 108 | const variables = { | |
| 109 | APP_ID: mobile.android.packageName, TRAIL_ID: String(trail.id), TRAIL_NAME: trail.name, | |
| 110 | E2E_EMAIL: QA_SOCIAL_ACCOUNT.email, E2E_PASSWORD: QA_SOCIAL_ACCOUNT.sessionSecret, | |
| 111 | REVIEW_TEXT: review, EDITED_REVIEW_TEXT: edit, | |
| 112 | PROFILE_END_TEXT: `${endpoint.mi.toFixed(2)} mi · ${endElevation}`, | |
| 113 | INLINE_PROFILE_END_TEXT: `${endpoint.mi.toFixed(2)} mi, ${endElevation}`, | |
| 114 | GPX_NAME: `${trail.name.replace(/[^a-z0-9]+/gi, '-').replace(/^-|-$/g, '').slice(0, 80)}.gpx`, | |
| 115 | } | |
| 116 | let reviewId: number | undefined | |
| 117 | for (const phase of ['maps', 'utilities', 'sharing', 'directions', 'directions-map', 'review-create', 'review-edit', 'review-delete', 'appearance'] as const) { | |
| 118 | const report = join(root, `${phase}.xml`) | |
| 119 | if (phase === 'appearance') await adb(['shell', 'cmd', 'uimode', 'night', 'yes'], 'system-dark-appearance') | |
| 120 | await command([ | |
| 121 | 'maestro', `--device=${device}`, '--no-ansi', 'test', | |
| 122 | ...Object.entries(variables).flatMap(([key, value]) => ['--env', `${key}=${value}`]), | |
| 123 | '--format', 'junit', '--output', report, '--debug-output', join(root, 'debug', phase), | |
| 124 | '--test-output-dir', join(root, 'tests', phase), `.maestro/native-trail/${phase}.yaml`, | |
| 125 | ], phase, maestroEnvironment()) | |
| 126 | const summary = maestroReportSummary(await readFile(report, 'utf8')) | |
| 127 | if (!summary.tests || summary.failures || summary.skipped) throw new Error(`Native Android ${phase} did not execute and pass every required test.`) | |
| 128 | if (phase.startsWith('review-')) { | |
| 129 | const saved = await reviews() | |
| 130 | await writeFile(join(root, `${phase}-persistence.json`), JSON.stringify(saved, null, 2)) | |
| 131 | const action = phase === 'review-create' ? 'create' : phase === 'review-edit' ? 'edit' : 'delete' | |
| 132 | reviewId = assertNativeReviewPersistence(saved, action, action === 'create' ? review : edit, reviewId) | |
| 133 | } | |
| 134 | if (phase === 'utilities') { | |
| 135 | const files = await adb(['shell', 'run-as', mobile.android.packageName, 'find', '.', '-type', 'f'], 'export-files') | |
| 136 | const paths = files.split(/\r?\n/).filter(path => /^[a-zA-Z0-9_./-]+\.gpx$/.test(path)) | |
| 137 | let exported = false | |
| 138 | for (const path of paths) { | |
| 139 | const xml = await adb(['exec-out', 'run-as', mobile.android.packageName, 'cat', path], 'exported-gpx') | |
| 140 | if (xml.includes('<gpx ') && xml.includes('<trkseg>') && xml.includes(`<trkpt lat="${first[0]}" lon="${first[1]}"`)) exported = true | |
| 141 | } | |
| 142 | if (!exported) throw new Error('The native GPX export did not contain the recorded route coordinates.') | |
| 143 | } | |
| 144 | if (phase === 'directions' || phase === 'directions-map') { | |
| 145 | const activities = await adb(['shell', 'dumpsys', 'activity', 'activities'], `${phase}-activities`) | |
| 146 | const resumed = activities.split('\n').filter(line => /(?:mResumedActivity|topResumedActivity):/.test(line)) | |
| 147 | const expectedHandler = /\b(?:com\.google\.android\.apps\.maps|com\.android\.chrome)\// | |
| 148 | const unfinishedActivity = /(?:ResolverActivity|ChooserActivity|FirstRunActivity)\b/ | |
| 149 | if (!resumed.length || resumed.some(line => !expectedHandler.test(line) || unfinishedActivity.test(line))) | |
| 150 | throw new Error('Directions did not reach ready Chrome or Google Maps on the stock Play test device.') | |
| 151 | } | |
| 152 | await platformEvents(phase) | |
| 153 | } | |
| 154 | console.log('Native Android maps, sticky actions, location, GPX, route customization, actual share-link paste and persisted review create/edit/delete passed.') | |
| 155 | } | |
| 156 | finally { | |
| 157 | await adb(['logcat', '-d', '-s', 'CraftNativeAndroid:V', 'AndroidRuntime:E'], 'app-runtime').catch(() => {}) | |
| 158 | await adb(['shell', 'uiautomator', 'dump', '/sdcard/wildloop-trail-final.xml'], 'final-tree').then(() => | |
| 159 | adb(['pull', '/sdcard/wildloop-trail-final.xml', join(root, 'final-tree.xml')], 'pull-final-tree')).catch(() => {}) | |
| 160 | for (const port of forwards) await adb(['reverse', '--remove', `tcp:${port}`], `remove-reverse-${port}`).catch(() => {}) | |
| 161 | qa.kill('SIGTERM') | |
| 162 | await qa.exited | |
| 163 | } | |
| 164 | } | |
| 165 | ||
| 166 | if (import.meta.main) await runNativeTrailAndroidE2e() | |
| @@ -4,7 +4,7 @@ import process from 'node:process' | ||
| 4 | 4 | import { generateXcodeProject, resolveXcodegen } from '@stacksjs/actions/build/ios-toolchain' |
| 5 | 5 | import { prepareIosSimulatorDriver, serializeSimulatorSteps } from '@stacksjs/mobile/testing' |
| 6 | 6 | import mobile from '../config/mobile' |
| 7 | import { nativeTrailJourneys } from '../tests/native/trail-journey' | |
| 7 | import { nativeTrailJourneys, selectNativeTrailJourneys } from '../tests/native/trail-journey' | |
| 8 | 8 | import { QA_PORTS } from '../tests/browser/qa-ports' |
| 9 | 9 | import { QA_SOCIAL_ACCOUNT } from '../tests/browser/qa-social-account' |
| 10 | 10 | import { decodeRouteParts, primaryRoutePart } from '../resources/functions/trail-geometry' |
| @@ -50,8 +50,23 @@ export async function runNativeTrailE2e(): Promise<void> { | ||
| 50 | 50 | if(detail.trail?.hasGeometry){trail=detail.trail;break} |
| 51 | 51 | } |
| 52 | 52 | if(!trail)throw new Error('Native trail QA requires its seeded recorded route.') |
| 53 | const profileResponse=await fetch(`${origin}/api/trails/${trail.id}/profile`) | |
| 54 | if(!profileResponse.ok)throw new Error('Native iOS QA requires the trail profile endpoint to be available.') | |
| 55 | const profile=await profileResponse.json() as {points:Array<{mi:number,ft:number|null}>} | |
| 56 | const endpoint=profile.points?.at(-1) | |
| 57 | if(!endpoint || profile.points.length<2 || !Number.isFinite(endpoint.mi) || (endpoint.ft!==null && !Number.isFinite(endpoint.ft))) | |
| 58 | throw new Error('Native iOS QA requires a selectable recorded route profile.') | |
| 59 | const endElevation=endpoint.ft===null ? 'elevation unavailable' : `${endpoint.ft} ft` | |
| 53 | 60 | const review=`Native QA ${crypto.randomUUID().slice(0,8)}: clear signposts and a beautiful ridge.` |
| 54 | const journeys=nativeTrailJourneys({trailId:trail.id,trailName:trail.name,email:QA_SOCIAL_ACCOUNT.email,password:QA_SOCIAL_ACCOUNT.sessionSecret,review,edit:' Updated after a return visit.'}) | |
| 61 | const edit='Updated after a return visit.' | |
| 62 | const readReviews=async()=>{ | |
| 63 | const response=await fetch(`${origin}/api/trails/${trail.id}/reviews`,{cache:'no-store'}) | |
| 64 | if(!response.ok)throw new Error(`Reading persisted reviews failed: ${response.status}`) | |
| 65 | return ((await response.json()) as any).reviews as Array<{id:number,content:string,rating:number}> | |
| 66 | } | |
| 67 | if((await readReviews()).length)throw new Error('The isolated native trail must start without reviews.') | |
| 68 | let createdReviewId:number|undefined | |
| 69 | const journeys=selectNativeTrailJourneys(nativeTrailJourneys({trailId:trail.id,trailName:trail.name,email:QA_SOCIAL_ACCOUNT.email,password:QA_SOCIAL_ACCOUNT.sessionSecret,review,edit,profileEndText:`${endpoint.mi.toFixed(2)} mi · ${endElevation}`,inlineProfileEndText:`${endpoint.mi.toFixed(2)} mi, ${endElevation}`}),process.env.NATIVE_TRAIL_GROUP) | |
| 55 | 70 | const controlPort=QA_PORTS.app+15 |
| 56 | 71 | const driver=prepareIosSimulatorDriver({...mobile.ios,simulator:{output:join(root,'driver'),projectName:'WildloopTrailDriver',bundleIdPrefix:'org.wildloop.trailqa',controlUrl:`http://127.0.0.1:${controlPort}`}}) |
| 57 | 72 | control=Bun.serve({hostname:'127.0.0.1',port:controlPort,idleTimeout:120,async fetch(request){ |
| @@ -60,23 +75,34 @@ export async function runNativeTrailE2e(): Promise<void> { | ||
| 60 | 75 | if(url.pathname==='/exec') { |
| 61 | 76 | try { |
| 62 | 77 | const hook=url.searchParams.get('name') |
| 78 | if(hook==='verify-review-created' || hook==='verify-review-edited' || hook==='verify-review-deleted') { | |
| 79 | const rows=await readReviews() | |
| 80 | await writeFile(join(root,`${hook}-persistence.json`),JSON.stringify(rows,null,2)) | |
| 81 | if(hook==='verify-review-deleted') { | |
| 82 | if(rows.length)throw new Error('The native deletion did not remove the persisted review.') | |
| 83 | return new Response('PASS deleted review is absent from persistence') | |
| 84 | } | |
| 85 | if(rows.length!==1)throw new Error('The native review action must persist exactly one review.') | |
| 86 | if(hook==='verify-review-created') { | |
| 87 | if(rows[0].content!==review || rows[0].rating!==5)throw new Error('The created review text and rating did not persist.') | |
| 88 | createdReviewId=rows[0].id | |
| 89 | return new Response('PASS created review text and rating persisted') | |
| 90 | } | |
| 91 | if(rows[0].id!==createdReviewId || rows[0].rating!==4 || rows[0].content!==edit) | |
| 92 | throw new Error('The native edit did not retain the review identity and persist its new text and rating.') | |
| 93 | return new Response('PASS edited review retained its identity and persisted') | |
| 94 | } | |
| 95 | if(hook==='appearance-dark' || hook==='appearance-light') { | |
| 96 | const appearance=hook==='appearance-dark' ? 'dark' : 'light' | |
| 97 | await command(['xcrun','simctl','ui',device,'appearance',appearance],`${hook}.log`) | |
| 98 | return new Response(`PASS platform appearance ${appearance}`) | |
| 99 | } | |
| 63 | 100 | if(hook==='seed-location') { |
| 64 | 101 | const first=primaryRoutePart([...decodeRouteParts(trail.geometry),...(Array.isArray(trail.pieceRoutes)?trail.pieceRoutes:[])])[0] |
| 65 | 102 | if(!first)throw new Error('The seeded trail has no recorded route start.') |
| 66 | 103 | await command(['xcrun','simctl','location',device,'set',`${first[0]},${first[1]}`],'location-seed.log') |
| 67 | 104 | return new Response('PASS owned simulator location set to the seeded public trailhead') |
| 68 | 105 | } |
| 69 | if(hook==='verify-share') { | |
| 70 | console.log('Native trail QA: inspect the owned simulator share clipboard') | |
| 71 | const pasted=Bun.spawnSync(['xcrun','simctl','pbpaste',device],{timeout:15000,killSignal:'SIGKILL',maxBuffer:1048576}) | |
| 72 | await writeFile(join(root,'sharing-clipboard.log'),JSON.stringify({exitCode:pasted.exitCode,signalCode:pasted.signalCode,stdout:pasted.stdout.toString(),stderr:pasted.stderr.toString()},null,2)) | |
| 73 | if(pasted.exitCode!==0 || !pasted.stdout.toString().includes(`https://wildloop.org/trail/${trail.id}`)) { | |
| 74 | const verbose=Bun.spawnSync(['xcrun','simctl','pbpaste','-v',device],{timeout:15000,killSignal:'SIGKILL',maxBuffer:1048576}) | |
| 75 | await writeFile(join(root,'sharing-clipboard-types.log'),JSON.stringify({exitCode:verbose.exitCode,signalCode:verbose.signalCode,stdout:verbose.stdout.toString(),stderr:verbose.stderr.toString()},null,2)) | |
| 76 | throw new Error('The native share action did not copy this trail link. Inspect sharing-clipboard.log and sharing-clipboard-types.log.') | |
| 77 | } | |
| 78 | return new Response('PASS native share copied the trail link') | |
| 79 | } | |
| 80 | 106 | if(hook==='verify-gpx') { |
| 81 | 107 | const located=Bun.spawnSync(['xcrun','simctl','get_app_container',device,mobile.ios.bundleId,'data']) |
| 82 | 108 | if(located.exitCode)throw new Error('Could not inspect the owned simulator application container.') |
| @@ -154,7 +180,7 @@ export async function runNativeTrailE2e(): Promise<void> { | ||
| 154 | 180 | } |
| 155 | 181 | } |
| 156 | 182 | if(failures.length)throw new Error(`Native trail QA failed: ${failures.join(', ')}. Inspect the separate XCTest results, driver logs and screenshots.`) |
| 157 | console.log('Native trail maps, sticky save, review lifecycle, sharing, GPX export, route planner and location journeys passed.') | |
| 183 | console.log(`Native trail journeys passed: ${journeys.map(journey=>journey.name).join(', ')}.`) | |
| 158 | 184 | } |
| 159 | 185 | finally { |
| 160 | 186 | if(nativeRun && nativeRun.exitCode===null)nativeRun.kill() |
| @@ -0,0 +1,58 @@ | ||
| 1 | import { dirname, join, resolve } from 'node:path' | |
| 2 | import { createAndroidEmulator } from 'craft-native/android' | |
| 3 | ||
| 4 | /** Run the installed app on a disposable device owned by the published SDK. */ | |
| 5 | export async function runOwnedAndroidE2e(suite: string | undefined): Promise<void> { | |
| 6 | if (suite !== 'baseline' && suite !== 'native-trail') | |
| 7 | throw new Error('Select the baseline or native-trail Android journey.') | |
| 8 | const sdkRoot = process.env.ANDROID_HOME ?? process.env.ANDROID_SDK_ROOT | |
| 9 | const sdkmanager = Bun.which('sdkmanager') | |
| 10 | const avdmanager = Bun.which('avdmanager') | |
| 11 | if (!sdkRoot || !sdkmanager || !avdmanager) | |
| 12 | throw new Error('Install the normal Android SDK command-line tools before native QA.') | |
| 13 | const root = resolve(`storage/framework/runtime/e2e/${suite === 'native-trail' ? 'native-trail-android' : 'android'}/emulator`) | |
| 14 | const abi = process.arch === 'arm64' ? 'arm64-v8a' : 'x86_64' | |
| 15 | const session = await createAndroidEmulator({ | |
| 16 | sdkRoot, sdkmanager, avdmanager, | |
| 17 | image: `system-images;android-35;google_apis_playstore;${abi}`, | |
| 18 | acceleration: 'hardware', acceptLicenses: true, artifactsDirectory: root, | |
| 19 | }) | |
| 20 | try { | |
| 21 | const args = suite === 'native-trail' | |
| 22 | ? ['bun', 'scripts/run-native-trail-android-e2e.ts', '--skip-build'] | |
| 23 | : ['bun', 'scripts/run-mobile-e2e.ts', 'android', '--skip-build'] | |
| 24 | const test = Bun.spawn(args, { | |
| 25 | env: { | |
| 26 | ...session.env, PATH: `${dirname(session.adb)}:${session.env.PATH ?? ''}`, | |
| 27 | NATIVE_QA_ANDROID_DEVICE: session.serial, | |
| 28 | }, | |
| 29 | stdout: 'inherit', stderr: 'inherit', | |
| 30 | }) | |
| 31 | const status = await test.exited | |
| 32 | if (status !== 0) { | |
| 33 | for (const [name, args] of [ | |
| 34 | ['device-state', ['get-state']], | |
| 35 | ['platform-events', ['logcat', '-b', 'events', '-d', '-v', 'threadtime']], | |
| 36 | ['platform-properties', ['shell', 'getprop']], | |
| 37 | ] as const) { | |
| 38 | try { | |
| 39 | const diagnostic = Bun.spawn([session.adb, '-s', session.serial, ...args], { | |
| 40 | env: session.env, stdout: 'pipe', stderr: 'pipe', signal: AbortSignal.timeout(10_000), | |
| 41 | }) | |
| 42 | const [code, stdout, stderr] = await Promise.all([ | |
| 43 | diagnostic.exited, new Response(diagnostic.stdout).text(), new Response(diagnostic.stderr).text(), | |
| 44 | ]) | |
| 45 | await Bun.write(join(root, `failure-${name}.json`), JSON.stringify({ code, stdout, stderr }) + '\n') | |
| 46 | } | |
| 47 | catch (cause) { console.warn(`Could not retain Android ${name}: ${cause}`) } | |
| 48 | } | |
| 49 | throw new Error(`Android ${suite} acceptance failed with exit ${status}.`) | |
| 50 | } | |
| 51 | } | |
| 52 | finally { | |
| 53 | await session.dispose() | |
| 54 | await Bun.write(join(root, 'disposal.json'), JSON.stringify({ serial: session.serial, disposed: true }) + '\n') | |
| 55 | } | |
| 56 | } | |
| 57 | ||
| 58 | if (import.meta.main) await runOwnedAndroidE2e(process.argv[2]) | |
| @@ -8,11 +8,11 @@ | ||
| 8 | 8 | "prod": "bun --hot run dist/dev.js" |
| 9 | 9 | }, |
| 10 | 10 | "dependencies": { |
| 11 | "@stacksjs/logging": "^0.75.219", | |
| 12 | "@stacksjs/queue": "^0.75.219", | |
| 13 | "@stacksjs/router": "^0.75.219", | |
| 14 | "@stacksjs/storage": "^0.75.219", | |
| 15 | "@stacksjs/utils": "^0.75.219" | |
| 11 | "@stacksjs/logging": "^0.75.221", | |
| 12 | "@stacksjs/queue": "^0.75.221", | |
| 13 | "@stacksjs/router": "^0.75.221", | |
| 14 | "@stacksjs/storage": "^0.75.221", | |
| 15 | "@stacksjs/utils": "^0.75.221" | |
| 16 | 16 | }, |
| 17 | 17 | "workspaces": [ |
| 18 | 18 | "../core/*" |
| @@ -73,6 +73,7 @@ export { nativeDestination, nativeShortcutDestination } from '../../../resources | ||
| 73 | 73 | export { exportNativeGpx } from '../../../resources/functions/native-export' |
| 74 | 74 | export { nativeMapParts, nativeRouteRegion, nativeZoomRegion } from '../../../resources/functions/native-map' |
| 75 | 75 | export { nativeOAuthCallback } from '../../../resources/functions/native-oauth' |
| 76 | export { createNativeRecordingCheckpoint } from '../../../resources/functions/native-recording-checkpoint' | |
| 76 | 77 | export { nativeRecordingDuration, movingSeconds, recordingSamples, recordingFigures, recordingPayload } from '../../../resources/functions/native-recording' |
| 77 | 78 | export type { NativeRecording } from '../../../resources/functions/native-recording' |
| 78 | 79 | export { returnToServerFromBundledCopy, serverPath, serverURLFor, holdOnBundledPage, NATIVE_REMOTE_FILE, NATIVE_REMOTE_RETRY_MS, NATIVE_REMOTE_PROBE_TIMEOUT_MS } from '../../../resources/functions/native-remote' |
| @@ -136,7 +137,7 @@ export { normalizeTagKey, readTags, featureFromTag, trailFeatures, trailHighligh | ||
| 136 | 137 | export type { TrailFeature, TrailNotice, FeatureTrail } from '../../../resources/functions/trail-features' |
| 137 | 138 | export { routePartsFromSegments, lineLengthMeters, encodeRouteParts, decodeRouteParts, primaryRoutePart, ROUTE_SNAP_METERS } from '../../../resources/functions/trail-geometry' |
| 138 | 139 | export type { RoutePair, RouteNetwork } from '../../../resources/functions/trail-geometry' |
| 139 | export { trailSegmentMiles, trailDirectionSigns, trailSurfaceTypes, trailGpx } from '../../../resources/functions/trail-map' | |
| 140 | export { trailheadPosition, trailSegmentMiles, trailDirectionSigns, trailSurfaceTypes, trailGpx, trailRouteStyle } from '../../../resources/functions/trail-map' | |
| 140 | 141 | export { placeOfLocation, whereLocation, wholeTrailFigures, trailFacts, countryLabel, trailBreadcrumbs, trailFaq, exploreNearLinks, ratingSummary, forecastDayLabel, forecastBar, NEAR_PLACE_PREFIX } from '../../../resources/functions/trail-page' |
| 141 | 142 | export type { PageTrail, TrailFigures, Crumb, FaqEntry, LinkGroup, RatingSummary } from '../../../resources/functions/trail-page' |
| 142 | 143 | export { parsePhotoList, trailGallery } from '../../../resources/functions/trail-photos' |
| @@ -295,6 +295,7 @@ declare global { | ||
| 295 | 295 | const nativeRouteRegion: typeof autoImports.nativeRouteRegion |
| 296 | 296 | const nativeZoomRegion: typeof autoImports.nativeZoomRegion |
| 297 | 297 | const nativeOAuthCallback: typeof autoImports.nativeOAuthCallback |
| 298 | const createNativeRecordingCheckpoint: typeof autoImports.createNativeRecordingCheckpoint | |
| 298 | 299 | const nativeRecordingDuration: typeof autoImports.nativeRecordingDuration |
| 299 | 300 | const movingSeconds: typeof autoImports.movingSeconds |
| 300 | 301 | const recordingSamples: typeof autoImports.recordingSamples |
| @@ -475,10 +476,12 @@ declare global { | ||
| 475 | 476 | const decodeRouteParts: typeof autoImports.decodeRouteParts |
| 476 | 477 | const primaryRoutePart: typeof autoImports.primaryRoutePart |
| 477 | 478 | const ROUTE_SNAP_METERS: typeof autoImports.ROUTE_SNAP_METERS |
| 479 | const trailheadPosition: typeof autoImports.trailheadPosition | |
| 478 | 480 | const trailSegmentMiles: typeof autoImports.trailSegmentMiles |
| 479 | 481 | const trailDirectionSigns: typeof autoImports.trailDirectionSigns |
| 480 | 482 | const trailSurfaceTypes: typeof autoImports.trailSurfaceTypes |
| 481 | 483 | const trailGpx: typeof autoImports.trailGpx |
| 484 | const trailRouteStyle: typeof autoImports.trailRouteStyle | |
| 482 | 485 | const placeOfLocation: typeof autoImports.placeOfLocation |
| 483 | 486 | const whereLocation: typeof autoImports.whereLocation |
| 484 | 487 | const wholeTrailFigures: typeof autoImports.wholeTrailFigures |
| @@ -46,19 +46,19 @@ | ||
| 46 | 46 | "dev": "./dev local" |
| 47 | 47 | }, |
| 48 | 48 | "dependencies": { |
| 49 | "@stacksjs/config": "^0.75.219", | |
| 50 | "@stacksjs/dns": "^0.75.219", | |
| 51 | "@stacksjs/logging": "^0.75.219", | |
| 52 | "@stacksjs/path": "^0.75.219", | |
| 53 | "@stacksjs/queue": "^0.75.219", | |
| 54 | "@stacksjs/router": "^0.75.219", | |
| 55 | "@stacksjs/storage": "^0.75.219", | |
| 56 | "@stacksjs/utils": "^0.75.219", | |
| 57 | "@stacksjs/validation": "^0.75.219" | |
| 49 | "@stacksjs/config": "^0.75.221", | |
| 50 | "@stacksjs/dns": "^0.75.221", | |
| 51 | "@stacksjs/logging": "^0.75.221", | |
| 52 | "@stacksjs/path": "^0.75.221", | |
| 53 | "@stacksjs/queue": "^0.75.221", | |
| 54 | "@stacksjs/router": "^0.75.221", | |
| 55 | "@stacksjs/storage": "^0.75.221", | |
| 56 | "@stacksjs/utils": "^0.75.221", | |
| 57 | "@stacksjs/validation": "^0.75.221" | |
| 58 | 58 | }, |
| 59 | 59 | "devDependencies": { |
| 60 | "@stacksjs/cli": "^0.75.219", | |
| 60 | "@stacksjs/cli": "^0.75.221", | |
| 61 | 61 | "better-dx": "^0.2.25", |
| 62 | "@stacksjs/env": "^0.75.219" | |
| 62 | "@stacksjs/env": "^0.75.221" | |
| 63 | 63 | } |
| 64 | 64 | } |
| @@ -233,6 +233,12 @@ buddy env:check # validate env configuration | ||
| 233 | 233 | buddy env:check --file .env.production # check a specific environment's file |
| 234 | 234 | ``` |
| 235 | 235 | |
| 236 | `getEnv()` fails a whole-file read if any encrypted entry cannot be decrypted. | |
| 237 | Restore the environment's matching private key through `.env.keys` or its | |
| 238 | `DOTENV_PRIVATE_KEY_<ENVIRONMENT>` process secret. A request for one plaintext | |
| 239 | key remains readable independently. `buddy deploy` stops on an incomplete or | |
| 240 | unreadable environment instead of deploying a partial map. | |
| 241 | ||
| 236 | 242 | ## Tenant isolation on a shared box |
| 237 | 243 | |
| 238 | 244 | When several projects share one server - one owns it, the rest attach with |
| @@ -83,6 +83,28 @@ Based on `cz-git` UserConfig: | ||
| 83 | 83 | |
| 84 | 84 | ## Git Utilities |
| 85 | 85 | |
| 86 | `gitCredentialEnvironment({ url, username, password })` from `@stacksjs/git` | |
| 87 | returns environment variables for a child Git process. Pass the caller's base | |
| 88 | environment as its second argument, then merge the result after that base. | |
| 89 | Existing runtime Git configuration is preserved. The HTTP(S) URL must identify one repository without | |
| 90 | userinfo, query or fragment. The helper supplies credentials only for that | |
| 91 | protocol, host and repository path, refuses redirects for that URL, disables | |
| 92 | interactive prompts, and writes no credential or Git config file. Never log | |
| 93 | the returned environment. Use a credential scoped to the repository and the | |
| 94 | operation; this helper does not grant server permissions. | |
| 95 | ||
| 96 | ```typescript | |
| 97 | import { gitCredentialEnvironment } from '@stacksjs/git' | |
| 98 | ||
| 99 | const child = Bun.spawn(['git', 'ls-remote', 'origin'], { | |
| 100 | cwd: workspace, | |
| 101 | env: { ...process.env, ...gitCredentialEnvironment({ url: repositoryUrl, username: 'x-access-token', password: jobToken }, process.env) }, | |
| 102 | }) | |
| 103 | ``` | |
| 104 | ||
| 105 | The child and its descendants receive the credential. Use this for trusted | |
| 106 | job steps, never to transfer a host credential into an isolated guest. | |
| 107 | ||
| 86 | 108 | `buddy release` atomically pushes its commit and version tag to the branch's |
| 87 | 109 | configured upstream remote and branch. A worktree on `codex/example` tracking |
| 88 | 110 | `origin/main` releases to `origin/main`; it does not create a remote feature |
| @@ -21,7 +21,7 @@ Browser runtime bindings are documented separately in `stacks-auto-imports`. | ||
| 21 | 21 | | `@stacksjs/auth` | [`stacks-auth`](../stacks-auth/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/index.ts) | 115 | |
| 22 | 22 | | `@stacksjs/browser` | [`stacks-browser`](../stacks-browser/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/browser/src/index.ts) | 8 | |
| 23 | 23 | | `@stacksjs/browser-extension` | [`stacks-browser-extension`](../stacks-browser-extension/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/browser-extension/src/index.ts) | 7 | |
| 24 | | `@stacksjs/buddy` | [`stacks-buddy`](../stacks-buddy/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/buddy/src/index.ts) | 149 | | |
| 24 | | `@stacksjs/buddy` | [`stacks-buddy`](../stacks-buddy/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/buddy/src/index.ts) | 150 | | |
| 25 | 25 | | `@stacksjs/build` | [`stacks-build`](../stacks-build/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/build/src/index.ts) | 2 | |
| 26 | 26 | | `@stacksjs/cache` | [`stacks-cache`](../stacks-cache/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/cache/src/index.ts) | 11 | |
| 27 | 27 | | `@stacksjs/calendar-api` | [`stacks-calendar`](../stacks-calendar/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/calendar-api/src/index.ts) | 2 | |
| @@ -45,14 +45,14 @@ Browser runtime bindings are documented separately in `stacks-auto-imports`. | ||
| 45 | 45 | | `@stacksjs/docs` | [`stacks-docs`](../stacks-docs/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/docs/src/index.ts) | 2 | |
| 46 | 46 | | `@stacksjs/email` | [`stacks-email`](../stacks-email/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/email/src/index.ts) | 21 | |
| 47 | 47 | | `@stacksjs/enums` | [`stacks-enums`](../stacks-enums/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/enums/src/index.ts) | 3 | |
| 48 | | `@stacksjs/env` | [`stacks-env`](../stacks-env/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/src/index.ts) | 27 | | |
| 48 | | `@stacksjs/env` | [`stacks-env`](../stacks-env/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/src/index.ts) | 28 | | |
| 49 | 49 | | `@stacksjs/error-handling` | [`stacks-error-handling`](../stacks-error-handling/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/error-handling/src/index.ts) | 10 | |
| 50 | 50 | | `@stacksjs/events` | [`stacks-events`](../stacks-events/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/events/src/index.ts) | 12 | |
| 51 | 51 | | `@stacksjs/faker` | [`stacks-faker`](../stacks-faker/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/faker/src/index.ts) | 1 | |
| 52 | 52 | | `@stacksjs/feature-flags` | [`stacks-feature-flags`](../stacks-feature-flags/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/feature-flags/src/index.ts) | 7 | |
| 53 | 53 | | `@stacksjs/features` | [`stacks-features`](../stacks-features/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/features/src/index.ts) | 1 | |
| 54 | 54 | | `@stacksjs/forms` | [`stacks-forms`](../stacks-forms/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/forms/src/index.ts) | 4 | |
| 55 | | `@stacksjs/git` | [`stacks-git`](../stacks-git/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/git/src/index.ts) | 1 | | |
| 55 | | `@stacksjs/git` | [`stacks-git`](../stacks-git/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/git/src/index.ts) | 2 | | |
| 56 | 56 | | `@stacksjs/github` | [`stacks-git`](../stacks-git/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/github/src/index.ts) | 7 | |
| 57 | 57 | | `@stacksjs/health` | [`stacks-health`](../stacks-health/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/health/src/index.ts) | 2 | |
| 58 | 58 | | `@stacksjs/http` | [`stacks-http`](../stacks-http/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/http/src/index.ts) | 2 | |
| @@ -92,7 +92,7 @@ Browser runtime bindings are documented separately in `stacks-auto-imports`. | ||
| 92 | 92 | | `@stacksjs/tinker` | [`stacks-repl`](../stacks-repl/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/tinker/src/index.ts) | 1 | |
| 93 | 93 | | `@stacksjs/tunnel` | [`stacks-tunnel`](../stacks-tunnel/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/tunnel/src/index.ts) | 1 | |
| 94 | 94 | | `@stacksjs/types` | [`stacks-types`](../stacks-types/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/types/src/index.ts) | 2 | |
| 95 | | `@stacksjs/ui` | [`stacks-ui`](../stacks-ui/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ui/src/index.ts) | 5 | | |
| 95 | | `@stacksjs/ui` | [`stacks-ui`](../stacks-ui/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ui/src/index.ts) | 6 | | |
| 96 | 96 | | `@stacksjs/utils` | [`stacks-utils`](../stacks-utils/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/utils/src/index.ts) | 15 | |
| 97 | 97 | | `@stacksjs/validation` | [`stacks-validation`](../stacks-validation/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/validation/src/index.ts) | 17 | |
| 98 | 98 | | `@stacksjs/video` | [`stacks-video`](../stacks-video/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/video/src/index.ts) | 3 | |
| @@ -215,7 +215,7 @@ Read the entrypoint and package export map for re-exported or type-only surfaces | ||
| 215 | 215 | |
| 216 | 216 | Local source modules: [src/commands/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/buddy/src/commands/index.ts). |
| 217 | 217 | |
| 218 | Evidence directory: [149 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/buddy/tests). These files are evidence pointers, not a claim that all tests were run for this catalog. | |
| 218 | Evidence directory: [150 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/buddy/tests). These files are evidence pointers, not a claim that all tests were run for this catalog. | |
| 219 | 219 | |
| 220 | 220 | ### @stacksjs/build |
| 221 | 221 | |
| @@ -443,7 +443,7 @@ Read the entrypoint and package export map for re-exported or type-only surfaces | ||
| 443 | 443 | |
| 444 | 444 | Local source modules: [src/cli.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/src/cli.ts), [src/crypto.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/src/crypto.ts), [src/deployment.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/src/deployment.ts), [src/integrations.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/src/integrations.ts), [src/numeric-keys.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/src/numeric-keys.ts), [src/parser.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/src/parser.ts), [src/plaintext.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/src/plaintext.ts), [src/plugin.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/src/plugin.ts), [src/runtime.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/src/runtime.ts), [src/tenants.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/src/tenants.ts), [src/types.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/src/types.ts), [src/utils.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/src/utils.ts). |
| 445 | 445 | |
| 446 | Evidence directory: [27 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/tests). These files are evidence pointers, not a claim that all tests were run for this catalog. | |
| 446 | Evidence directory: [28 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/env/tests). These files are evidence pointers, not a claim that all tests were run for this catalog. | |
| 447 | 447 | |
| 448 | 448 | ### @stacksjs/error-handling |
| 449 | 449 | |
| @@ -509,7 +509,9 @@ Guidance: [`stacks-git`](../stacks-git/SKILL.md). Source: [storage/framework/cor | ||
| 509 | 509 | |
| 510 | 510 | Read the entrypoint and package export map for re-exported or type-only surfaces. |
| 511 | 511 | |
| 512 | Evidence directory: [1 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/git/tests). These files are evidence pointers, not a claim that all tests were run for this catalog. | |
| 512 | Local source modules: [src/credentials.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/git/src/credentials.ts). | |
| 513 | ||
| 514 | Evidence directory: [2 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/git/tests). These files are evidence pointers, not a claim that all tests were run for this catalog. | |
| 513 | 515 | |
| 514 | 516 | ### @stacksjs/github |
| 515 | 517 | |
| @@ -887,7 +889,7 @@ Declared runtime exports: `CssEngine`, `buildPageSequence`, `paginatorVariant`, | ||
| 887 | 889 | |
| 888 | 890 | Local source modules: [src/fonts.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ui/src/fonts.ts), [src/pagination.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ui/src/pagination.ts). |
| 889 | 891 | |
| 890 | Evidence directory: [5 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ui/tests). These files are evidence pointers, not a claim that all tests were run for this catalog. | |
| 892 | Evidence directory: [6 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ui/tests). These files are evidence pointers, not a claim that all tests were run for this catalog. | |
| 891 | 893 | |
| 892 | 894 | ### @stacksjs/utils |
| 893 | 895 | |
| @@ -2,7 +2,7 @@ | ||
| 2 | 2 | "publisher": "Stacks", |
| 3 | 3 | "name": "vscode-stacks", |
| 4 | 4 | "displayName": "Stacks", |
| 5 | "version": "0.75.219", | |
| 5 | "version": "0.75.221", | |
| 6 | 6 | "description": "Stacks, built into VS Code: stx templates, pickier linting and formatting, encrypted env files, the dev server preview and every buddy command. No other extensions required.", |
| 7 | 7 | "license": "MIT", |
| 8 | 8 | "funding": "https://github.com/sponsors/chrisbbreuer", |
| @@ -12,40 +12,93 @@ | ||
| 12 | 12 | const mcReplay = useReactiveProp<number>('replay', 0) |
| 13 | 13 | const mcReveals = derived(() => [mcReplay()]) |
| 14 | 14 | const mcCelebrate = useReactiveProp<boolean>('celebrate', true) |
| 15 | const mcSurface = useRef('membershipSurface') | |
| 16 | const mcCanHover = useMediaQuery('(hover: hover) and (pointer: fine)') | |
| 17 | const mcReduceMotion = usePreferredReducedMotion() | |
| 18 | let mcStopHover: (() => void) | undefined | |
| 19 | ||
| 20 | onMount(() => { | |
| 21 | const surface = mcSurface.current as HTMLElement | null | |
| 22 | if (!surface) return | |
| 23 | ||
| 24 | // Measure the stationary hit area, never the rotating card, to avoid feedback jitter. | |
| 25 | const reset = () => { | |
| 26 | surface.style.setProperty('--mc-rotate-x', '0deg') | |
| 27 | surface.style.setProperty('--mc-rotate-y', '0deg') | |
| 28 | surface.style.setProperty('--mc-light-x', '50%') | |
| 29 | surface.style.setProperty('--mc-light-y', '50%') | |
| 30 | surface.style.setProperty('--mc-hover', '0') | |
| 31 | } | |
| 32 | mcStopHover = effect(() => { | |
| 33 | reset() | |
| 34 | if (!mcCanHover() || mcReduceMotion()) return | |
| 35 | const mouse = useMouse({ target: surface, touch: false, resetOnLeave: true }) | |
| 36 | const unsubscribe = mouse.subscribe(({ clientX, clientY, isInside }) => { | |
| 37 | if (!isInside) return reset() | |
| 38 | const bounds = surface.getBoundingClientRect() | |
| 39 | if (!bounds.width || !bounds.height) return reset() | |
| 40 | const x = Math.max(0, Math.min(1, (clientX - bounds.left) / bounds.width)) | |
| 41 | const y = Math.max(0, Math.min(1, (clientY - bounds.top) / bounds.height)) | |
| 42 | surface.style.setProperty('--mc-rotate-x', `${(0.5 - y) * 12}deg`) | |
| 43 | surface.style.setProperty('--mc-rotate-y', `${(x - 0.5) * 16}deg`) | |
| 44 | surface.style.setProperty('--mc-light-x', `${x * 100}%`) | |
| 45 | surface.style.setProperty('--mc-light-y', `${y * 100}%`) | |
| 46 | surface.style.setProperty('--mc-hover', '1') | |
| 47 | }) | |
| 48 | return () => { | |
| 49 | unsubscribe() | |
| 50 | mouse.stop() | |
| 51 | reset() | |
| 52 | } | |
| 53 | }) | |
| 54 | }) | |
| 55 | onDestroy(() => mcStopHover?.()) | |
| 56 | ||
| 15 | 57 | const mcPieces = [...Array(32).keys()].map(id => ({ |
| 16 | 58 | id, |
| 17 | 59 | style: `--mc-x:${(id * 37 + 11) % 100}%;--mc-drift:${(id * 29 % 160) - 80}px;--mc-delay:${id % 8 * 65}ms;--mc-turn:${180 + id * 47}deg;--mc-color:var(--membership-card-confetti-${id % 4 + 1},${['#99f6e4', '#e2e8f0', '#5eead4', '#94a3b8'][id % 4]})`, |
| 18 | 60 | })) |
| 19 | 61 | </script> |
| 20 | 62 | |
| 21 | <div :for="reveal in mcReveals" :key="reveal" class="relative mx-auto w-full mc-stage" :class="mcCelebrate ? 'mc-celebrate' : ''" data-membership-reveal> | |
| 22 | <div :if="mcCelebrate" class="absolute inset-0 overflow-hidden pointer-events-none mc-confetti" aria-hidden="true"> | |
| 23 | <span :for="piece in mcPieces" :key="piece.id" class="mc-piece" :style="piece.style"></span> | |
| 24 | </div> | |
| 25 | <article class="overflow-hidden relative mx-auto w-full mc-metal" :aria-label="mcLabel" data-membership-card> | |
| 26 | <div class="absolute inset-0 pointer-events-none mc-grain" aria-hidden="true"></div> | |
| 27 | <div class="absolute inset-0 pointer-events-none mc-shine" aria-hidden="true"></div> | |
| 28 | <div class="flex relative gap-4 items-start justify-between"> | |
| 29 | <img :if="mcLogo" class="mc-logo" :src="mcLogo" :alt="mcBrand" width="96" height="38" /> | |
| 30 | <span :if="!mcLogo" class="font-bold mc-wordmark" :text="mcBrand"></span> | |
| 31 | </div> | |
| 32 | <p class="relative mt-3 text-xs mc-label" :text="mcLabel"></p> | |
| 33 | <div class="relative mc-holder"> | |
| 34 | <div class="mc-person"> | |
| 35 | <p class="mb-2 text-xs mc-label" :text="mcNameLabel"></p> | |
| 36 | <p class="mc-engraving" :style="{ fontFamily: mcNameFont }" :text="mcName" data-membership-name></p> | |
| 63 | <div class="relative mx-auto w-full mc-stage" data-membership-reveal> | |
| 64 | <div ref="membershipSurface" class="mc-surface"> | |
| 65 | <div :for="reveal in mcReveals" :key="reveal" :class="mcCelebrate ? 'mc-celebrate' : ''"> | |
| 66 | <div :if="mcCelebrate" class="absolute inset-0 overflow-hidden pointer-events-none mc-confetti" aria-hidden="true"> | |
| 67 | <span :for="piece in mcPieces" :key="piece.id" class="mc-piece" :style="piece.style"></span> | |
| 37 | 68 | </div> |
| 38 | <div :if="mcMemberId" class="mc-identity"> | |
| 39 | <p class="text-xs mc-label" :text="mcMemberIdLabel"></p> | |
| 40 | <p class="font-semibold mc-serial" :text="mcMemberId" data-membership-id></p> | |
| 69 | <div class="mc-tilt"> | |
| 70 | <article class="overflow-hidden relative mx-auto w-full mc-metal" :aria-label="mcLabel" data-membership-card> | |
| 71 | <div class="absolute inset-0 pointer-events-none mc-grain" aria-hidden="true"></div> | |
| 72 | <div class="absolute inset-0 pointer-events-none mc-reflection" aria-hidden="true"></div> | |
| 73 | <div class="absolute inset-0 pointer-events-none mc-shine" aria-hidden="true"></div> | |
| 74 | <div class="flex relative gap-4 items-start justify-between"> | |
| 75 | <img :if="mcLogo" class="mc-logo" :src="mcLogo" :alt="mcBrand" width="96" height="38" /> | |
| 76 | <span :if="!mcLogo" class="font-bold mc-wordmark" :text="mcBrand"></span> | |
| 77 | </div> | |
| 78 | <p class="relative mt-3 text-xs mc-label" :text="mcLabel"></p> | |
| 79 | <div class="relative mc-holder"> | |
| 80 | <div class="mc-person"> | |
| 81 | <p class="mb-2 text-xs mc-label" :text="mcNameLabel"></p> | |
| 82 | <p class="mc-engraving" :style="{ fontFamily: mcNameFont }" :text="mcName" data-membership-name></p> | |
| 83 | </div> | |
| 84 | <div :if="mcMemberId" class="mc-identity"> | |
| 85 | <p class="text-xs mc-label" :text="mcMemberIdLabel"></p> | |
| 86 | <p class="font-semibold mc-serial" :text="mcMemberId" data-membership-id></p> | |
| 87 | </div> | |
| 88 | </div> | |
| 89 | <p :if="mcDetail" class="relative mt-5 text-xs mc-detail" :text="mcDetail"></p> | |
| 90 | </article> | |
| 41 | 91 | </div> |
| 42 | 92 | </div> |
| 43 | <p :if="mcDetail" class="relative mt-5 text-xs mc-detail" :text="mcDetail"></p> | |
| 44 | </article> | |
| 93 | </div> | |
| 45 | 94 | </div> |
| 46 | 95 | |
| 47 | 96 | <style> |
| 48 | 97 | .mc-stage { max-width: 28rem; padding: 2rem 0 2.5rem; perspective: 1000px; } |
| 98 | .mc-surface { isolation: isolate; } | |
| 99 | .mc-tilt { transform: perspective(1000px) rotateX(var(--mc-rotate-x, 0deg)) rotateY(var(--mc-rotate-y, 0deg)) translateY(calc(var(--mc-hover, 0) * -3px)); transition: transform 450ms cubic-bezier(0.2, 0.75, 0.25, 1); } | |
| 100 | .mc-surface:hover .mc-tilt { transition-duration: 160ms; } | |
| 101 | .mc-reflection { opacity: calc(var(--mc-hover, 0) * 0.65); background: radial-gradient(ellipse at var(--mc-light-x, 50%) var(--mc-light-y, 50%), #ffffff38, #ffffff0a 42%, transparent 72%); transition: opacity 350ms ease; } | |
| 49 | 102 | .mc-metal { |
| 50 | 103 | box-sizing: border-box; min-height: 15.5rem; padding: clamp(1.25rem, 5vw, 2rem); border-radius: 1.25rem; |
| 51 | 104 | color: var(--membership-card-color, #e2e8f0); border: 1px solid var(--membership-card-border, #87939d); |
| @@ -79,10 +132,14 @@ | ||
| 79 | 132 | .mc-celebrate .mc-metal { animation: mc-arrive 1100ms cubic-bezier(0.16, 1, 0.3, 1) both; } |
| 80 | 133 | .mc-celebrate .mc-shine { animation: mc-polish 1700ms ease-in-out 650ms both; } |
| 81 | 134 | .mc-celebrate .mc-piece { animation: mc-fall 2400ms cubic-bezier(0.2, 0.65, 0.45, 1) var(--mc-delay) both; } |
| 82 | .mc-metal:hover .mc-shine { transform: translateX(150%) skewX(-20deg); transition: transform 1400ms ease; } | |
| 135 | } | |
| 136 | @media (hover: none), (pointer: coarse) { | |
| 137 | .mc-tilt { transform: none; transition: none; } | |
| 138 | .mc-reflection { display: none; } | |
| 83 | 139 | } |
| 84 | 140 | @media (prefers-reduced-motion: reduce) { |
| 85 | .mc-confetti, .mc-shine { display: none; } | |
| 141 | .mc-confetti, .mc-shine, .mc-reflection { display: none; } | |
| 142 | .mc-tilt { transform: none; transition: none; } | |
| 86 | 143 | } |
| 87 | 144 | @keyframes mc-arrive { from { opacity: 0; transform: translateY(32px) rotateX(16deg) rotateY(-12deg) scale(0.92); } to { opacity: 1; transform: none; } } |
| 88 | 145 | @keyframes mc-polish { from { transform: translateX(-150%) skewX(-20deg); } to { transform: translateX(150%) skewX(-20deg); } } |
| @@ -1,22 +1,5 @@ | ||
| 1 | 1 | { |
| 2 | 2 | "models": [ |
| 3 | { | |
| 4 | "id": "achievement", | |
| 5 | "name": "Achievement", | |
| 6 | "icon": "tablecells.fill", | |
| 7 | "hasDedicatedPage": false, | |
| 8 | "category": "userland", | |
| 9 | "dashboard": { | |
| 10 | "highlight": true | |
| 11 | } | |
| 12 | }, | |
| 13 | { | |
| 14 | "id": "activity-comment", | |
| 15 | "name": "ActivityComment", | |
| 16 | "icon": "tablecells.fill", | |
| 17 | "hasDedicatedPage": false, | |
| 18 | "category": "userland" | |
| 19 | }, | |
| 20 | 3 | { |
| 21 | 4 | "id": "analytics-event", |
| 22 | 5 | "name": "AnalyticsEvent", |
| @@ -132,13 +115,6 @@ | ||
| 132 | 115 | "enabled": false |
| 133 | 116 | } |
| 134 | 117 | }, |
| 135 | { | |
| 136 | "id": "card-attachment", | |
| 137 | "name": "CardAttachment", | |
| 138 | "icon": "tablecells.fill", | |
| 139 | "hasDedicatedPage": false, | |
| 140 | "category": "data" | |
| 141 | }, | |
| 142 | 118 | { |
| 143 | 119 | "id": "card-comment", |
| 144 | 120 | "name": "CardComment", |
| @@ -186,40 +162,6 @@ | ||
| 186 | 162 | "highlight": true |
| 187 | 163 | } |
| 188 | 164 | }, |
| 189 | { | |
| 190 | "id": "challenge", | |
| 191 | "name": "Challenge", | |
| 192 | "icon": "tablecells.fill", | |
| 193 | "hasDedicatedPage": false, | |
| 194 | "category": "userland", | |
| 195 | "dashboard": { | |
| 196 | "highlight": true | |
| 197 | } | |
| 198 | }, | |
| 199 | { | |
| 200 | "id": "club", | |
| 201 | "name": "Club", | |
| 202 | "icon": "tablecells.fill", | |
| 203 | "hasDedicatedPage": false, | |
| 204 | "category": "userland", | |
| 205 | "dashboard": { | |
| 206 | "highlight": true | |
| 207 | } | |
| 208 | }, | |
| 209 | { | |
| 210 | "id": "club-invite", | |
| 211 | "name": "ClubInvite", | |
| 212 | "icon": "tablecells.fill", | |
| 213 | "hasDedicatedPage": false, | |
| 214 | "category": "userland" | |
| 215 | }, | |
| 216 | { | |
| 217 | "id": "club-member", | |
| 218 | "name": "ClubMember", | |
| 219 | "icon": "tablecells.fill", | |
| 220 | "hasDedicatedPage": false, | |
| 221 | "category": "userland" | |
| 222 | }, | |
| 223 | 165 | { |
| 224 | 166 | "id": "comment", |
| 225 | 167 | "name": "Comment", |
| @@ -241,13 +183,6 @@ | ||
| 241 | 183 | "hasDedicatedPage": false, |
| 242 | 184 | "category": "data" |
| 243 | 185 | }, |
| 244 | { | |
| 245 | "id": "content-report", | |
| 246 | "name": "ContentReport", | |
| 247 | "icon": "tablecells.fill", | |
| 248 | "hasDedicatedPage": false, | |
| 249 | "category": "userland" | |
| 250 | }, | |
| 251 | 186 | { |
| 252 | 187 | "id": "coupon", |
| 253 | 188 | "name": "Coupon", |
| @@ -285,13 +220,6 @@ | ||
| 285 | 220 | "highlight": true |
| 286 | 221 | } |
| 287 | 222 | }, |
| 288 | { | |
| 289 | "id": "custom-route", | |
| 290 | "name": "CustomRoute", | |
| 291 | "icon": "tablecells.fill", | |
| 292 | "hasDedicatedPage": false, | |
| 293 | "category": "userland" | |
| 294 | }, | |
| 295 | 223 | { |
| 296 | 224 | "id": "delivery-route", |
| 297 | 225 | "name": "DeliveryRoute", |
| @@ -322,13 +250,6 @@ | ||
| 322 | 250 | "highlight": true |
| 323 | 251 | } |
| 324 | 252 | }, |
| 325 | { | |
| 326 | "id": "device-push-token", | |
| 327 | "name": "DevicePushToken", | |
| 328 | "icon": "tablecells.fill", | |
| 329 | "hasDedicatedPage": false, | |
| 330 | "category": "userland" | |
| 331 | }, | |
| 332 | 253 | { |
| 333 | 254 | "id": "digital-delivery", |
| 334 | 255 | "name": "DigitalDelivery", |
| @@ -383,44 +304,6 @@ | ||
| 383 | 304 | "enabled": false |
| 384 | 305 | } |
| 385 | 306 | }, |
| 386 | { | |
| 387 | "id": "event", | |
| 388 | "name": "Event", | |
| 389 | "icon": "tablecells.fill", | |
| 390 | "hasDedicatedPage": false, | |
| 391 | "category": "userland", | |
| 392 | "dashboard": { | |
| 393 | "highlight": true | |
| 394 | } | |
| 395 | }, | |
| 396 | { | |
| 397 | "id": "event-entrant", | |
| 398 | "name": "EventEntrant", | |
| 399 | "icon": "tablecells.fill", | |
| 400 | "hasDedicatedPage": false, | |
| 401 | "category": "userland" | |
| 402 | }, | |
| 403 | { | |
| 404 | "id": "event-lap", | |
| 405 | "name": "EventLap", | |
| 406 | "icon": "tablecells.fill", | |
| 407 | "hasDedicatedPage": false, | |
| 408 | "category": "userland" | |
| 409 | }, | |
| 410 | { | |
| 411 | "id": "feedback-token", | |
| 412 | "name": "FeedbackToken", | |
| 413 | "icon": "tablecells.fill", | |
| 414 | "hasDedicatedPage": false, | |
| 415 | "category": "data" | |
| 416 | }, | |
| 417 | { | |
| 418 | "id": "follow", | |
| 419 | "name": "Follow", | |
| 420 | "icon": "tablecells.fill", | |
| 421 | "hasDedicatedPage": false, | |
| 422 | "category": "userland" | |
| 423 | }, | |
| 424 | 307 | { |
| 425 | 308 | "id": "form", |
| 426 | 309 | "name": "Form", |
| @@ -466,13 +349,6 @@ | ||
| 466 | 349 | "hasDedicatedPage": true, |
| 467 | 350 | "category": "system" |
| 468 | 351 | }, |
| 469 | { | |
| 470 | "id": "kudos", | |
| 471 | "name": "Kudos", | |
| 472 | "icon": "tablecells.fill", | |
| 473 | "hasDedicatedPage": false, | |
| 474 | "category": "userland" | |
| 475 | }, | |
| 476 | 352 | { |
| 477 | 353 | "id": "label", |
| 478 | 354 | "name": "Label", |
| @@ -772,28 +648,11 @@ | ||
| 772 | 648 | "name": "Review", |
| 773 | 649 | "icon": "star.fill", |
| 774 | 650 | "hasDedicatedPage": true, |
| 775 | "category": "userland", | |
| 776 | "dashboard": { | |
| 777 | "highlight": true | |
| 778 | } | |
| 779 | }, | |
| 780 | { | |
| 781 | "id": "route-effort", | |
| 782 | "name": "RouteEffort", | |
| 783 | "icon": "tablecells.fill", | |
| 784 | "hasDedicatedPage": false, | |
| 785 | "category": "userland", | |
| 651 | "category": "commerce", | |
| 786 | 652 | "dashboard": { |
| 787 | 653 | "highlight": true |
| 788 | 654 | } |
| 789 | 655 | }, |
| 790 | { | |
| 791 | "id": "saved-trail", | |
| 792 | "name": "SavedTrail", | |
| 793 | "icon": "tablecells.fill", | |
| 794 | "hasDedicatedPage": false, | |
| 795 | "category": "userland" | |
| 796 | }, | |
| 797 | 656 | { |
| 798 | 657 | "id": "sender-domain", |
| 799 | 658 | "name": "SenderDomain", |
| @@ -957,50 +816,6 @@ | ||
| 957 | 816 | "enabled": false |
| 958 | 817 | } |
| 959 | 818 | }, |
| 960 | { | |
| 961 | "id": "territory", | |
| 962 | "name": "Territory", | |
| 963 | "icon": "tablecells.fill", | |
| 964 | "hasDedicatedPage": false, | |
| 965 | "category": "userland", | |
| 966 | "dashboard": { | |
| 967 | "highlight": true | |
| 968 | } | |
| 969 | }, | |
| 970 | { | |
| 971 | "id": "territory-history", | |
| 972 | "name": "TerritoryHistory", | |
| 973 | "icon": "tablecells.fill", | |
| 974 | "hasDedicatedPage": false, | |
| 975 | "category": "userland" | |
| 976 | }, | |
| 977 | { | |
| 978 | "id": "territory-stats", | |
| 979 | "name": "TerritoryStats", | |
| 980 | "icon": "tablecells.fill", | |
| 981 | "hasDedicatedPage": false, | |
| 982 | "category": "userland" | |
| 983 | }, | |
| 984 | { | |
| 985 | "id": "trail", | |
| 986 | "name": "Trail", | |
| 987 | "icon": "tablecells.fill", | |
| 988 | "hasDedicatedPage": false, | |
| 989 | "category": "userland", | |
| 990 | "dashboard": { | |
| 991 | "highlight": true | |
| 992 | } | |
| 993 | }, | |
| 994 | { | |
| 995 | "id": "trail-ingest-shard", | |
| 996 | "name": "TrailIngestShard", | |
| 997 | "icon": "tablecells.fill", | |
| 998 | "hasDedicatedPage": false, | |
| 999 | "category": "userland", | |
| 1000 | "dashboard": { | |
| 1001 | "highlight": false | |
| 1002 | } | |
| 1003 | }, | |
| 1004 | 819 | { |
| 1005 | 820 | "id": "transaction", |
| 1006 | 821 | "name": "Transaction", |
| @@ -1011,13 +826,6 @@ | ||
| 1011 | 826 | "highlight": true |
| 1012 | 827 | } |
| 1013 | 828 | }, |
| 1014 | { | |
| 1015 | "id": "trip-plan", | |
| 1016 | "name": "TripPlan", | |
| 1017 | "icon": "tablecells.fill", | |
| 1018 | "hasDedicatedPage": false, | |
| 1019 | "category": "userland" | |
| 1020 | }, | |
| 1021 | 829 | { |
| 1022 | 830 | "id": "usage-event", |
| 1023 | 831 | "name": "UsageEvent", |
| @@ -1030,42 +838,10 @@ | ||
| 1030 | 838 | "name": "User", |
| 1031 | 839 | "icon": "person.fill", |
| 1032 | 840 | "hasDedicatedPage": true, |
| 1033 | "category": "userland" | |
| 1034 | }, | |
| 1035 | { | |
| 1036 | "id": "user-achievement", | |
| 1037 | "name": "UserAchievement", | |
| 1038 | "icon": "tablecells.fill", | |
| 1039 | "hasDedicatedPage": false, | |
| 1040 | "category": "userland" | |
| 1041 | }, | |
| 1042 | { | |
| 1043 | "id": "user-block", | |
| 1044 | "name": "UserBlock", | |
| 1045 | "icon": "tablecells.fill", | |
| 1046 | "hasDedicatedPage": false, | |
| 1047 | "category": "userland" | |
| 1048 | }, | |
| 1049 | { | |
| 1050 | "id": "user-notification", | |
| 1051 | "name": "UserNotification", | |
| 1052 | "icon": "tablecells.fill", | |
| 1053 | "hasDedicatedPage": false, | |
| 1054 | "category": "userland" | |
| 1055 | }, | |
| 1056 | { | |
| 1057 | "id": "user-privacy-setting", | |
| 1058 | "name": "UserPrivacySetting", | |
| 1059 | "icon": "tablecells.fill", | |
| 1060 | "hasDedicatedPage": false, | |
| 1061 | "category": "userland" | |
| 1062 | }, | |
| 1063 | { | |
| 1064 | "id": "user-stats", | |
| 1065 | "name": "UserStats", | |
| 1066 | "icon": "tablecells.fill", | |
| 1067 | "hasDedicatedPage": false, | |
| 1068 | "category": "userland" | |
| 841 | "category": "data", | |
| 842 | "dashboard": { | |
| 843 | "highlight": true | |
| 844 | } | |
| 1069 | 845 | }, |
| 1070 | 846 | { |
| 1071 | 847 | "id": "waitlist-product", |
| @@ -8,12 +8,12 @@ | ||
| 8 | 8 | "preview": "bunpress preview" |
| 9 | 9 | }, |
| 10 | 10 | "dependencies": { |
| 11 | "@stacksjs/alias": "^0.75.219", | |
| 12 | "@stacksjs/cli": "^0.75.219", | |
| 13 | "@stacksjs/config": "^0.75.219", | |
| 14 | "@stacksjs/env": "^0.75.219", | |
| 15 | "@stacksjs/path": "^0.75.219", | |
| 16 | "@stacksjs/server": "^0.75.219", | |
| 17 | "@stacksjs/storage": "^0.75.219" | |
| 11 | "@stacksjs/alias": "^0.75.221", | |
| 12 | "@stacksjs/cli": "^0.75.221", | |
| 13 | "@stacksjs/config": "^0.75.221", | |
| 14 | "@stacksjs/env": "^0.75.221", | |
| 15 | "@stacksjs/path": "^0.75.221", | |
| 16 | "@stacksjs/server": "^0.75.221", | |
| 17 | "@stacksjs/storage": "^0.75.221" | |
| 18 | 18 | } |
| 19 | 19 | } |
| @@ -45,9 +45,9 @@ | ||
| 45 | 45 | "prepublishOnly": "bun run build" |
| 46 | 46 | }, |
| 47 | 47 | "dependencies": { |
| 48 | "@stacksjs/cache": "^0.75.219", | |
| 49 | "@stacksjs/database": "^0.75.219", | |
| 50 | "@stacksjs/error-handling": "^0.75.219" | |
| 48 | "@stacksjs/cache": "^0.75.221", | |
| 49 | "@stacksjs/database": "^0.75.221", | |
| 50 | "@stacksjs/error-handling": "^0.75.221" | |
| 51 | 51 | }, |
| 52 | 52 | "devDependencies": { |
| 53 | 53 | "better-dx": "^0.2.5" |
| @@ -48,10 +48,10 @@ | ||
| 48 | 48 | }, |
| 49 | 49 | "devDependencies": { |
| 50 | 50 | "better-dx": "^0.2.5", |
| 51 | "@stacksjs/logging": "^0.75.219", | |
| 52 | "@stacksjs/path": "^0.75.219", | |
| 53 | "@stacksjs/router": "^0.75.219", | |
| 54 | "@stacksjs/storage": "^0.75.219", | |
| 55 | "@stacksjs/utils": "^0.75.219" | |
| 51 | "@stacksjs/logging": "^0.75.221", | |
| 52 | "@stacksjs/path": "^0.75.221", | |
| 53 | "@stacksjs/router": "^0.75.221", | |
| 54 | "@stacksjs/storage": "^0.75.221", | |
| 55 | "@stacksjs/utils": "^0.75.221" | |
| 56 | 56 | } |
| 57 | 57 | } |
| @@ -49,7 +49,7 @@ export function nodesByTestId(node: any, found = new Map<string, string>()): Map | ||
| 49 | 49 | return found |
| 50 | 50 | } |
| 51 | 51 | |
| 52 | export async function start(route: string, params: Record<string, unknown> = {}, capabilities = ['fetch', 'secureStorage', 'haptics', 'lifecycle', 'dialog']): Promise<NativeSession> { | |
| 52 | export async function start(route: string, params: Record<string, unknown> = {}, capabilities = ['fetch', 'secureStorage', 'haptics', 'lifecycle', 'dialog'], keychain?: Map<string, string>): Promise<NativeSession> { | |
| 53 | 53 | const sent: any[] = [] |
| 54 | 54 | let callback: (raw: string) => void = () => {} |
| 55 | 55 | /* |
| @@ -103,7 +103,18 @@ export async function start(route: string, params: Record<string, unknown> = {}, | ||
| 103 | 103 | platform: 'ios', |
| 104 | 104 | capabilityProtocolVersion: 1, |
| 105 | 105 | capabilities, |
| 106 | postMessage: (raw: string) => sent.push(JSON.parse(raw)), | |
| 106 | postMessage: (raw: string) => { | |
| 107 | const message = JSON.parse(raw) | |
| 108 | sent.push(message) | |
| 109 | // Keep real bridge requests observable while modelling repeated native | |
| 110 | // keychain reads and credential changes between async UI steps. | |
| 111 | if (keychain && message.type === 'API_REQUEST' && message.payload?.module === 'SecureStorage' && message.payload.method === 'get') { | |
| 112 | void Promise.resolve().then(() => callback(JSON.stringify({ | |
| 113 | type: 'API_RESPONSE', correlationId: message.id, | |
| 114 | payload: { version: 1, requestId: message.id, data: keychain.get(message.payload.args[0]) ?? null }, | |
| 115 | }))) | |
| 116 | } | |
| 117 | }, | |
| 107 | 118 | onMessage: (receiver: typeof callback) => { callback = receiver }, |
| 108 | 119 | }, |
| 109 | 120 | } |
| @@ -1,7 +1,25 @@ | ||
| 1 | 1 | import type { SimulatorStep } from '@stacksjs/mobile/testing' |
| 2 | 2 | |
| 3 | export const nativeTrailJourneyGroups = { | |
| 4 | maps: ['maps'], | |
| 5 | reviews: ['reviews', 'review-delete'], | |
| 6 | utilities: ['sharing', 'gpx', 'planner', 'location'], | |
| 7 | navigation: ['directions', 'map-directions', 'appearance'], | |
| 8 | } as const | |
| 9 | ||
| 10 | /** Partition all required journeys while retaining review lifecycle state. */ | |
| 11 | export function selectNativeTrailJourneys<T extends {name:string}>(journeys:T[],group?:string):T[] { | |
| 12 | const grouped=Object.values(nativeTrailJourneyGroups).flat() | |
| 13 | if(grouped.length!==journeys.length || new Set(grouped).size!==journeys.length || journeys.some(journey=>!grouped.includes(journey.name as typeof grouped[number]))) | |
| 14 | throw new Error('Every native trail journey must belong to exactly one CI group.') | |
| 15 | if(!group)return journeys | |
| 16 | if(!Object.hasOwn(nativeTrailJourneyGroups,group))throw new Error(`Unknown native trail journey group: ${group}`) | |
| 17 | const names=nativeTrailJourneyGroups[group as keyof typeof nativeTrailJourneyGroups] | |
| 18 | return journeys.filter(journey=>(names as readonly string[]).includes(journey.name)) | |
| 19 | } | |
| 20 | ||
| 3 | 21 | /** Real native UI interactions against the isolated QA database. */ |
| 4 | export function nativeTrailJourneys(input:{trailId:number,trailName:string,email:string,password:string,review:string,edit:string}):{name:string,steps:SimulatorStep[],completion:string}[] { | |
| 22 | export function nativeTrailJourneys(input:{trailId:number,trailName:string,email:string,password:string,review:string,edit:string,profileEndText:string,inlineProfileEndText:string}):{name:string,steps:SimulatorStep[],completion:string}[] { | |
| 5 | 23 | const arrival:SimulatorStep[] = [ |
| 6 | 24 | {command:'launch'}, |
| 7 | 25 | {command:'expectid',text:'native-feed-scroll',seconds:15}, |
| @@ -26,27 +44,80 @@ const maps:SimulatorStep[] = [ | ||
| 26 | 44 | {command:'expectexact',text:'Save',seconds:30}, |
| 27 | 45 | {command:'tapid',text:'trail-save'}, |
| 28 | 46 | {command:'expect',text:'Saved',seconds:10}, |
| 47 | {command:'openurl',url:'wildloop://feed'}, | |
| 48 | {command:'expectid',text:'native-feed-scroll',seconds:15}, | |
| 49 | {command:'openurl',url:`wildloop://trail/${input.trailId}`}, | |
| 50 | {command:'expectexact',text:'Saved',seconds:30}, | |
| 29 | 51 | {command:'tapid',text:'trail-save'}, |
| 30 | 52 | {command:'expectexact',text:'Save',seconds:10}, |
| 53 | {command:'openurl',url:'wildloop://feed'}, | |
| 54 | {command:'expectid',text:'native-feed-scroll',seconds:15}, | |
| 55 | {command:'openurl',url:`wildloop://trail/${input.trailId}`}, | |
| 56 | {command:'expectexact',text:'Save',seconds:30}, | |
| 31 | 57 | {command:'scrollto',text:'Toggle elevation profile'}, |
| 32 | 58 | {command:'shot',name:'native-floating-profile-inline'}, |
| 59 | {command:'scrollto',text:'Explore elevation and position along the route'}, | |
| 60 | {command:'sliderid',id:'trail-elevation-slider',position:1}, | |
| 61 | {command:'expectexact',text:input.inlineProfileEndText,seconds:10}, | |
| 62 | {command:'shot',name:'native-inline-elevation-endpoint'}, | |
| 63 | {command:'tree',name:'native-inline-elevation-endpoint'}, | |
| 33 | 64 | {command:'tapid',text:'trail-profile-toggle'}, |
| 34 | 65 | {command:'expect',text:'Elevation profile collapsed',seconds:10}, |
| 35 | 66 | {command:'scrollto',text:'Toggle elevation profile'}, |
| 36 | 67 | {command:'shot',name:'native-profile-collapsed-inline'}, |
| 37 | 68 | {command:'tapid',text:'trail-profile-toggle'}, |
| 38 | 69 | {command:'expect',text:'Elevation profile expanded',seconds:10}, |
| 70 | {command:'expectexact',text:input.inlineProfileEndText,seconds:10}, | |
| 71 | {command:'shot',name:'native-profile-expanded-inline'}, | |
| 72 | {command:'scrollto',text:'Zoom in'}, | |
| 73 | {command:'tapid',text:'trail-zoom-in'}, | |
| 74 | {command:'shot',name:'native-inline-zoom-in'}, | |
| 75 | {command:'tapid',text:'trail-zoom-out'}, | |
| 76 | {command:'shot',name:'native-inline-zoom-out'}, | |
| 77 | {command:'tapid',text:'trail-fit-route'}, | |
| 78 | {command:'shot',name:'native-inline-fit-route'}, | |
| 79 | {command:'tapid',text:'trail-map-3d'}, | |
| 80 | {command:'expectexact',text:'2D',seconds:10}, | |
| 81 | {command:'shot',name:'native-inline-map-tilted'}, | |
| 82 | {command:'tapexact',text:'North'}, | |
| 83 | {command:'shot',name:'native-inline-map-north'}, | |
| 84 | {command:'tapid',text:'trail-map-3d'}, | |
| 85 | {command:'expectexact',text:'3D',seconds:10}, | |
| 86 | {command:'shot',name:'native-inline-map-flat'}, | |
| 87 | {command:'scrollto',text:'Map layer'}, | |
| 88 | {command:'tapid',text:'trail-map-layer'}, | |
| 89 | {command:'tapexact',text:'Satellite'}, | |
| 90 | {command:'expect',text:'Map layer: Satellite',seconds:10}, | |
| 91 | {command:'shot',name:'native-inline-satellite'}, | |
| 92 | ...['Hybrid','Street','Outdoors'].flatMap((layer):SimulatorStep[]=>[ | |
| 93 | {command:'tapid',text:'trail-map-layer'}, | |
| 94 | {command:'tapexact',text:layer}, | |
| 95 | {command:'expect',text:`Map layer: ${layer}`,seconds:10}, | |
| 96 | {command:'shot',name:`native-inline-layer-${layer.toLowerCase()}`}, | |
| 97 | ]), | |
| 39 | 98 | {command:'scrollto',text:'Expand map'}, |
| 40 | 99 | {command:'tapid',text:'trail-map-fullscreen'}, |
| 41 | 100 | {command:'expectid',text:'trail-full-map',seconds:10}, |
| 42 | 101 | {command:'shot',name:'native-floating-profile-fullscreen'}, |
| 43 | 102 | {command:'tree',name:'native-floating-profile-fullscreen'}, |
| 103 | {command:'sliderid',id:'map-elevation-slider',position:1}, | |
| 104 | {command:'expectexact',text:input.profileEndText,seconds:10}, | |
| 105 | {command:'shot',name:'native-elevation-endpoint'}, | |
| 106 | {command:'tree',name:'native-elevation-endpoint'}, | |
| 44 | 107 | {command:'tapid',text:'map-zoom-in'}, |
| 108 | {command:'shot',name:'native-fullscreen-zoom-in'}, | |
| 45 | 109 | {command:'tapid',text:'map-zoom-out'}, |
| 110 | {command:'shot',name:'native-fullscreen-zoom-out'}, | |
| 46 | 111 | {command:'tapid',text:'map-fit'}, |
| 112 | {command:'shot',name:'native-fullscreen-fit-route'}, | |
| 47 | 113 | {command:'tapid',text:'map-3d'}, |
| 48 | 114 | {command:'expectexact',text:'2D',seconds:10}, |
| 115 | {command:'shot',name:'native-fullscreen-map-tilted'}, | |
| 49 | 116 | {command:'tapid',text:'map-north'}, |
| 117 | {command:'shot',name:'native-fullscreen-map-north'}, | |
| 118 | {command:'tapid',text:'map-3d'}, | |
| 119 | {command:'expectexact',text:'3D',seconds:10}, | |
| 120 | {command:'shot',name:'native-fullscreen-map-flat'}, | |
| 50 | 121 | {command:'tapid',text:'map-profile-toggle'}, |
| 51 | 122 | {command:'expect',text:'Elevation profile collapsed',seconds:10}, |
| 52 | 123 | {command:'shot',name:'native-profile-collapsed'}, |
| @@ -61,8 +132,17 @@ const maps:SimulatorStep[] = [ | ||
| 61 | 132 | {command:'tapid',text:'map-layer'}, |
| 62 | 133 | {command:'tapexact',text:layer}, |
| 63 | 134 | {command:'expect',text:`Map layer: ${layer}`,seconds:10}, |
| 135 | {command:'shot',name:`native-fullscreen-layer-${layer.toLowerCase()}`}, | |
| 64 | 136 | ]), |
| 65 | 137 | {command:'back'}, |
| 138 | {command:'scrollto',text:'Weather'}, | |
| 139 | {command:'expectid',text:'trail-weather-heading',seconds:10}, | |
| 140 | {command:'shot',name:'native-trail-weather'}, | |
| 141 | {command:'tree',name:'native-trail-weather'}, | |
| 142 | {command:'scrollto',text:'Surface types'}, | |
| 143 | {command:'expectid',text:'trail-surfaces-heading',seconds:10}, | |
| 144 | {command:'shot',name:'native-trail-surfaces'}, | |
| 145 | {command:'tree',name:'native-trail-surfaces'}, | |
| 66 | 146 | {command:'expectid',text:'trail-sticky-actions',seconds:10}, |
| 67 | 147 | ] |
| 68 | 148 | const reviews:SimulatorStep[] = [ |
| @@ -79,27 +159,38 @@ const reviews:SimulatorStep[] = [ | ||
| 79 | 159 | {command:'scrollto',text:'Edit your review'}, |
| 80 | 160 | {command:'shot',name:'native-review-created'}, |
| 81 | 161 | {command:'tree',name:'native-review-created'}, |
| 162 | {command:'exec',name:'verify-review-created'}, | |
| 82 | 163 | {command:'tapid',text:'trail-write-review'}, |
| 83 | 164 | {command:'expectid',text:'trail-review-composer',seconds:10}, |
| 84 | {command:'typeid',id:'trail-review-content',text:input.edit}, | |
| 165 | {command:'tapid',text:'trail-review-star-4'}, | |
| 166 | {command:'tapid',text:'trail-review-content'}, | |
| 167 | {command:'longpressid',text:'trail-review-content'}, | |
| 168 | {command:'shot',name:'native-review-edit-menu'}, | |
| 169 | {command:'tree',name:'native-review-edit-menu'}, | |
| 170 | {command:'tap',text:'Select All'}, | |
| 171 | {command:'type',text:input.edit}, | |
| 172 | {command:'valueid',id:'trail-review-content',text:input.edit}, | |
| 85 | 173 | {command:'scrollto',text:'Save review'}, |
| 86 | 174 | {command:'tapid',text:'trail-review-submit'}, |
| 87 | 175 | {command:'expect',text:input.edit.trim(),seconds:15}, |
| 88 | 176 | {command:'shot',name:'native-review-edited'}, |
| 177 | {command:'exec',name:'verify-review-edited'}, | |
| 89 | 178 | ] |
| 90 | 179 | // Keep deletion observable even when editing fails earlier in its own phase. |
| 91 | 180 | // This removes the real review created above; the aggregate still fails if |
| 92 | 181 | // any part of the lifecycle fails. |
| 93 | 182 | const reviewDelete:SimulatorStep[] = [ |
| 94 | 183 | ...arrival, |
| 95 | {command:'expect',text:input.review,seconds:15}, | |
| 184 | {command:'expect',text:'Edit your review',seconds:15}, | |
| 96 | 185 | {command:'scrollto',text:'Delete your review'}, |
| 97 | 186 | {command:'tapid',text:'trail-delete-review'}, |
| 98 | 187 | {command:'tapexact',text:'Delete review'}, |
| 99 | 188 | {command:'expect',text:'Leave a review',seconds:15}, |
| 100 | 189 | {command:'absent',text:input.review}, |
| 190 | {command:'absent',text:input.edit.trim()}, | |
| 101 | 191 | {command:'shot',name:'native-review-deleted'}, |
| 102 | 192 | {command:'tree',name:'native-review-deleted'}, |
| 193 | {command:'exec',name:'verify-review-deleted'}, | |
| 103 | 194 | ] |
| 104 | 195 | const fullMap:SimulatorStep[] = [ |
| 105 | 196 | ...arrival, |
| @@ -113,7 +204,28 @@ const sharing:SimulatorStep[] = [ | ||
| 113 | 204 | {command:'shot',name:'native-share-sheet'}, |
| 114 | 205 | {command:'tree',name:'native-share-sheet'}, |
| 115 | 206 | {command:'tapexact',text:'Copy'}, |
| 116 | {command:'exec',name:'verify-share'}, | |
| 207 | {command:'openurl',url:'wildloop://search'}, | |
| 208 | {command:'expectid',text:'search-input',seconds:10}, | |
| 209 | {command:'tapid',text:'search-input'}, | |
| 210 | {command:'longpressid',text:'search-input'}, | |
| 211 | {command:'tap',text:'Paste'}, | |
| 212 | {command:'valueid',id:'search-input',text:`https://wildloop.org/trail/${input.trailId}`}, | |
| 213 | {command:'shot',name:'native-shared-link-pasted'}, | |
| 214 | {command:'tree',name:'native-shared-link-pasted'}, | |
| 215 | {command:'openurl',url:`wildloop://trail/${input.trailId}`}, | |
| 216 | {command:'expectid',text:'trail-sticky-actions',seconds:15}, | |
| 217 | ] | |
| 218 | const directions:SimulatorStep[] = [ | |
| 219 | ...arrival, | |
| 220 | {command:'tapid',text:'trail-directions'}, | |
| 221 | {command:'expectapp',text:'com.apple.Maps',seconds:30}, | |
| 222 | {command:'shot',name:'native-sticky-directions-handoff'}, | |
| 223 | ] | |
| 224 | const mapDirections:SimulatorStep[] = [ | |
| 225 | ...fullMap, | |
| 226 | {command:'tapid',text:'map-directions'}, | |
| 227 | {command:'expectapp',text:'com.apple.Maps',seconds:30}, | |
| 228 | {command:'shot',name:'native-map-directions-handoff'}, | |
| 117 | 229 | ] |
| 118 | 230 | const gpx:SimulatorStep[] = [ |
| 119 | 231 | ...fullMap, |
| @@ -123,6 +235,14 @@ const gpx:SimulatorStep[] = [ | ||
| 123 | 235 | {command:'tree',name:'native-gpx-share-sheet'}, |
| 124 | 236 | {command:'tapexact',text:'Copy'}, |
| 125 | 237 | {command:'exec',name:'verify-gpx'}, |
| 238 | {command:'openurl',url:`wildloop://trail/${input.trailId}`}, | |
| 239 | {command:'expectid',text:'trail-sticky-actions',seconds:15}, | |
| 240 | {command:'scrollto',text:'Export GPX'}, | |
| 241 | {command:'tapid',text:'trail-map-export'}, | |
| 242 | {command:'expectexact',text:'Copy',seconds:10}, | |
| 243 | {command:'shot',name:'native-inline-gpx-share-sheet'}, | |
| 244 | {command:'tapexact',text:'Copy'}, | |
| 245 | {command:'exec',name:'verify-gpx'}, | |
| 126 | 246 | ] |
| 127 | 247 | const planner:SimulatorStep[] = [ |
| 128 | 248 | ...fullMap, |
| @@ -136,21 +256,64 @@ const planner:SimulatorStep[] = [ | ||
| 136 | 256 | ] |
| 137 | 257 | const location:SimulatorStep[] = [ |
| 138 | 258 | ...fullMap, |
| 259 | {command:'tapid',text:'map-zoom-in'}, | |
| 260 | {command:'tapid',text:'map-3d'}, | |
| 261 | {command:'labelid',id:'map-3d',text:'2D'}, | |
| 262 | {command:'shot',name:'native-camera-before-location'}, | |
| 139 | 263 | {command:'exec',name:'seed-location'}, |
| 140 | 264 | {command:'tapid',text:'map-locate'}, |
| 141 | 265 | {command:'systemexpect',text:'Allow Once',seconds:10}, |
| 142 | 266 | {command:'systemtap',text:'Allow Once'}, |
| 143 | 267 | {command:'expect',text:'Your location is shown',seconds:20}, |
| 268 | {command:'labelid',id:'map-3d',text:'2D'}, | |
| 144 | 269 | {command:'tree',name:'native-location'}, |
| 145 | 270 | {command:'shot',name:'native-location'}, |
| 271 | {command:'openurl',url:`wildloop://trail/${input.trailId}`}, | |
| 272 | {command:'expectid',text:'trail-sticky-actions',seconds:15}, | |
| 273 | {command:'scrollto',text:'Zoom in'}, | |
| 274 | {command:'tapid',text:'trail-zoom-in'}, | |
| 275 | {command:'tapid',text:'trail-map-3d'}, | |
| 276 | {command:'labelid',id:'trail-map-3d',text:'2D'}, | |
| 277 | {command:'shot',name:'native-inline-camera-before-location'}, | |
| 278 | {command:'scrollto',text:'My location'}, | |
| 279 | {command:'tapid',text:'trail-map-locate'}, | |
| 280 | {command:'expect',text:'Your location is shown',seconds:20}, | |
| 281 | {command:'labelid',id:'trail-map-3d',text:'2D'}, | |
| 282 | {command:'shot',name:'native-inline-location'}, | |
| 283 | {command:'tree',name:'native-inline-location'}, | |
| 146 | 284 | ] |
| 147 | 285 | return [ |
| 148 | 286 | {name:'maps',steps:maps,completion:'PASS expectid trail-sticky-actions'}, |
| 149 | {name:'reviews',steps:reviews,completion:`PASS expect ${input.edit.trim()}`}, | |
| 150 | {name:'review-delete',steps:reviewDelete,completion:`PASS absent ${input.review}`}, | |
| 151 | {name:'sharing',steps:sharing,completion:'exec verify-share: PASS native share copied the trail link'}, | |
| 287 | {name:'reviews',steps:reviews,completion:'exec verify-review-edited: PASS edited review retained its identity and persisted'}, | |
| 288 | {name:'review-delete',steps:reviewDelete,completion:'exec verify-review-deleted: PASS deleted review is absent from persistence'}, | |
| 289 | {name:'sharing',steps:sharing,completion:'PASS expectid trail-sticky-actions'}, | |
| 290 | {name:'directions',steps:directions,completion:'PASS expectapp com.apple.Maps'}, | |
| 291 | {name:'map-directions',steps:mapDirections,completion:'PASS expectapp com.apple.Maps'}, | |
| 152 | 292 | {name:'gpx',steps:gpx,completion:'exec verify-gpx: PASS exported GPX contains the recorded route'}, |
| 153 | 293 | {name:'planner',steps:planner,completion:'PASS expectid trail-full-map'}, |
| 154 | 294 | {name:'location',steps:location,completion:'PASS expect Your location is shown'}, |
| 295 | {name:'appearance',steps:[ | |
| 296 | {command:'exec',name:'appearance-dark'}, | |
| 297 | {command:'launch'}, | |
| 298 | {command:'openurl',url:`wildloop://trail/${input.trailId}`}, | |
| 299 | {command:'expect',text:input.trailName,seconds:30}, | |
| 300 | {command:'expectid',text:'trail-sticky-actions',seconds:15}, | |
| 301 | {command:'shot',name:'native-dark-trail-overview'}, | |
| 302 | {command:'scrollto',text:'Toggle elevation profile'}, | |
| 303 | {command:'shot',name:'native-dark-inline-profile'}, | |
| 304 | {command:'scrollto',text:'Expand map'}, | |
| 305 | {command:'tapid',text:'trail-map-fullscreen'}, | |
| 306 | {command:'expectid',text:'trail-full-map',seconds:15}, | |
| 307 | {command:'shot',name:'native-dark-full-map'}, | |
| 308 | {command:'tapid',text:'map-profile-toggle'}, | |
| 309 | {command:'shot',name:'native-dark-collapsed-profile'}, | |
| 310 | {command:'back'}, | |
| 311 | {command:'scrollto',text:'Weather'}, | |
| 312 | {command:'shot',name:'native-dark-weather'}, | |
| 313 | {command:'scrollto',text:'Surface types'}, | |
| 314 | {command:'shot',name:'native-dark-surfaces'}, | |
| 315 | {command:'expectid',text:'trail-sticky-actions',seconds:15}, | |
| 316 | {command:'exec',name:'appearance-light'}, | |
| 317 | ],completion:'exec appearance-light: PASS platform appearance light'}, | |
| 155 | 318 | ] |
| 156 | 319 | } |
| @@ -2,7 +2,7 @@ import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs' | ||
| 2 | 2 | import { tmpdir } from 'node:os' |
| 3 | 3 | import { join } from 'node:path' |
| 4 | 4 | import { describe, expect, it } from 'bun:test' |
| 5 | import { deepLinkFlow, maestroEnvironment, maestroReportSummary, parseAdbDevices, requestedPlatform, resolveJavaHome, selectAndroidDeepLinkActivity, selectAndroidHomePackage, selectIosSimulator, signedInTargetURL, validateAnalyticsScriptCount, validateBundledFrontend, validateGeneratedNativeApp, validateIosAppBundle, validateNativeShellStyles, validateNativeTabLinks } from '../../scripts/run-mobile-e2e' | |
| 5 | import { assertAndroidPlatformHealthy, deepLinkFlow, maestroEnvironment, maestroReportSummary, parseAdbDevices, requestedPlatform, resolveJavaHome, selectAndroidDeepLinkActivity, selectIosSimulator, signedInTargetURL, validateAnalyticsScriptCount, validateBundledFrontend, validateGeneratedNativeApp, validateIosAppBundle, validateNativeShellStyles, validateNativeTabLinks } from '../../scripts/run-mobile-e2e' | |
| 6 | 6 | import { inferDevelopmentTeam, requiredIosAppPaths, requiresDevelopmentTeam, selectAvailableIphone } from '../../scripts/run-ios-device' |
| 7 | 7 | |
| 8 | 8 | function writeNativeNavigationFixtures(output: string, intercepted = ''): void { |
| @@ -118,9 +118,10 @@ describe('mobile E2E runner', () => { | ||
| 118 | 118 | expect(selectAndroidDeepLinkActivity(output, 'org.missing.app')).toBeNull() |
| 119 | 119 | }) |
| 120 | 120 | |
| 121 | it('selects the Android home package without assuming a launcher vendor', () => { | |
| 122 | expect(selectAndroidHomePackage('com.google.android.apps.nexuslauncher/.NexusLauncherActivity\n')).toBe('com.google.android.apps.nexuslauncher') | |
| 123 | expect(selectAndroidHomePackage('')).toBeNull() | |
| 121 | it('rejects emulator crashes and ANRs rather than hiding the system launcher', () => { | |
| 122 | expect(() => assertAndroidPlatformHealthy('am_resume_activity: org.wildloop.app')).not.toThrow() | |
| 123 | expect(() => assertAndroidPlatformHealthy('am_anr: com.android.launcher3')).toThrow('invalidates native acceptance') | |
| 124 | expect(() => assertAndroidPlatformHealthy('am_crash: org.wildloop.app')).toThrow('invalidates native acceptance') | |
| 124 | 125 | }) |
| 125 | 126 | |
| 126 | 127 | it('accepts only supported platform arguments', () => { |
| @@ -171,7 +172,17 @@ describe('mobile E2E runner', () => { | ||
| 171 | 172 | <testcase name="pass" /> |
| 172 | 173 | <testcase name="fail"><failure>not visible</failure></testcase> |
| 173 | 174 | </testsuite> |
| 174 | `)).toEqual({ failures: 1, tests: 2 }) | |
| 175 | `)).toEqual({ failures: 1, tests: 2, skipped: 0 }) | |
| 176 | }) | |
| 177 | ||
| 178 | it('distinguishes skipped device journeys from executed passes', () => { | |
| 179 | expect(maestroReportSummary(` | |
| 180 | <testsuite tests="3" errors="1" skipped="1"> | |
| 181 | <testcase name="pass" /> | |
| 182 | <testcase name="not executed"><skipped /></testcase> | |
| 183 | <testcase name="device disconnected"><error>offline</error></testcase> | |
| 184 | </testsuite> | |
| 185 | `)).toEqual({ failures: 1, tests: 3, skipped: 1 }) | |
| 175 | 186 | }) |
| 176 | 187 | |
| 177 | 188 | it('rejects a mobile bundle that lost reactive page setup', () => { |
| @@ -288,3 +288,95 @@ describe('signed-out native mutation security', () => { | ||
| 288 | 288 | } finally {globalThis.fetch=previous} |
| 289 | 289 | }) |
| 290 | 290 | }) |
| 291 | ||
| 292 | describe('native account-bound operations', () => { | |
| 293 | it('requires a warm screen to reload after its account changes before an operation begins', async () => { | |
| 294 | let account: unknown = {user:{id:7},version:1} | |
| 295 | let token = 'account-seven' | |
| 296 | const fetched = recorder() | |
| 297 | try { | |
| 298 | const api = createNativeApi({snapshots:{get:()=>account},secureStorage:{get:()=>token}}) | |
| 299 | await api.send('/api/me') | |
| 300 | account = {user:{id:8},version:2} | |
| 301 | token = 'account-eight' | |
| 302 | await expect(api.bindSession()).rejects.toThrow('Your account changed') | |
| 303 | expect(fetched.calls).toHaveLength(1) | |
| 304 | } | |
| 305 | finally { fetched.restore() } | |
| 306 | }) | |
| 307 | ||
| 308 | it('refuses a warm screen after the bearer changes before the account snapshot arrives', async () => { | |
| 309 | let token = 'account-seven' | |
| 310 | const fetched = recorder() | |
| 311 | try { | |
| 312 | const api = createNativeApi({snapshots:{get:()=>({user:{id:7},version:1})},secureStorage:{get:()=>token}}) | |
| 313 | await api.send('/api/me') | |
| 314 | token = 'account-eight' | |
| 315 | await expect(api.bindSession()).rejects.toThrow('Your account changed') | |
| 316 | expect(fetched.calls).toHaveLength(1) | |
| 317 | } | |
| 318 | finally { fetched.restore() } | |
| 319 | }) | |
| 320 | ||
| 321 | it('keeps an identity check and its upload in the same account session', async () => { | |
| 322 | let account: unknown = {user:{id:7},version:1} | |
| 323 | let token = 'account-seven' | |
| 324 | const fetched = recorder({body:'{"user":{"id":7}}'}) | |
| 325 | try { | |
| 326 | const api = await createNativeApi({snapshots:{get:()=>account},secureStorage:{get:()=>token}}).bindSession() | |
| 327 | await api.send('/api/me') | |
| 328 | account = {user:{id:8},version:2} | |
| 329 | token = 'account-eight' | |
| 330 | await expect(api.send('/api/activities','POST',{upload_id:'run:owned'})).rejects.toThrow('Your account changed') | |
| 331 | expect(fetched.calls).toHaveLength(1) | |
| 332 | expect(fetched.calls[0].headers.Authorization).toBe('Bearer account-seven') | |
| 333 | } | |
| 334 | finally { fetched.restore() } | |
| 335 | }) | |
| 336 | ||
| 337 | it('works on a cold start without an account snapshot and detects a changed keychain', async () => { | |
| 338 | let token = 'cold-start-token' | |
| 339 | const fetched = recorder() | |
| 340 | try { | |
| 341 | const api = await createNativeApi({secureStorage:{get:()=>token}}).bindSession() | |
| 342 | await api.send('/api/me') | |
| 343 | token = 'replacement-token' | |
| 344 | await expect(api.send('/api/activities','POST',{})).rejects.toThrow('Your account changed') | |
| 345 | expect(fetched.calls).toHaveLength(1) | |
| 346 | } | |
| 347 | finally { fetched.restore() } | |
| 348 | }) | |
| 349 | ||
| 350 | it('does not bind a signed-out session or silently retarget after a keychain read', async () => { | |
| 351 | let account: unknown = {user:{id:7},version:1} | |
| 352 | const fetched = recorder() | |
| 353 | try { | |
| 354 | await expect(createNativeApi({secureStorage:{get:()=>null}}).bindSession()).rejects.toThrow('Sign in') | |
| 355 | const api = createNativeApi({snapshots:{get:()=>account},secureStorage:{get:async()=>{ | |
| 356 | account={user:{id:8},version:2} | |
| 357 | return 'previous-token' | |
| 358 | }}}) | |
| 359 | await expect(api.bindSession()).rejects.toThrow('Your account changed') | |
| 360 | expect(fetched.calls).toHaveLength(0) | |
| 361 | } | |
| 362 | finally { fetched.restore() } | |
| 363 | }) | |
| 364 | ||
| 365 | it('does not expose an old response after a cold-start bearer changes in flight', async () => { | |
| 366 | let token = 'account-seven' | |
| 367 | const previous = globalThis.fetch | |
| 368 | const bearers: string[] = [] | |
| 369 | globalThis.fetch = (async (url: any, init: any) => { | |
| 370 | expect(String(url)).toBe(`${WILDLOOP_ORIGIN}/api/me`) | |
| 371 | bearers.push(init.headers.Authorization) | |
| 372 | token = 'account-eight' | |
| 373 | return new Response('{"user":{"id":7}}') | |
| 374 | }) as typeof fetch | |
| 375 | try { | |
| 376 | const api = await createNativeApi({secureStorage:{get:()=>token}}).bindSession() | |
| 377 | await expect(api.send('/api/me')).rejects.toThrow('Your account changed') | |
| 378 | expect(bearers).toEqual(['Bearer account-seven']) | |
| 379 | } | |
| 380 | finally { globalThis.fetch = previous } | |
| 381 | }) | |
| 382 | }) | |
| @@ -1,17 +1,59 @@ | ||
| 1 | 1 | import { beforeAll, describe, expect, it } from 'bun:test' |
| 2 | 2 | import { readFileSync } from 'node:fs' |
| 3 | 3 | import { join } from 'node:path' |
| 4 | import { answer, compileNativeBundle, nativeBundle, nodesByTestId, requests, start, waitForRequest } from '../fixtures/nativeScreen' | |
| 4 | import { answer, compileNativeBundle, nativeBundle, nativeNodeProps, nodesByTestId, requests, start, waitForRequest, waitForText, type NativeSession } from '../fixtures/nativeScreen' | |
| 5 | 5 | |
| 6 | 6 | const ROOT = join(import.meta.dir, '../..') |
| 7 | 7 | |
| 8 | 8 | beforeAll(compileNativeBundle, 30000) |
| 9 | 9 | |
| 10 | function openManual(params: Record<string, unknown> = {}) { | |
| 11 | return start('ManualActivity',params,['fetch','secureStorage','crypto','haptics','lifecycle','dialog'],new Map([['auth_token','native-token']])) | |
| 12 | } | |
| 13 | async function supplyUUID(manual: NativeSession) { | |
| 14 | const uuid=await waitForRequest(manual,0,r=>r.payload.module==='Crypto' && r.payload.method==='randomUUID') | |
| 15 | answer(manual,uuid,'e8158db5-288f-4cb3-9540-016656f716c6') | |
| 16 | } | |
| 17 | ||
| 10 | 18 | describe('Wildloop native manual activity flow', () => { |
| 19 | it('keeps a system UUID and the exact manual payload when retrying a failed save',async()=>{ | |
| 20 | const manual=await start('ManualActivity',{},['fetch','secureStorage','crypto','haptics','lifecycle','dialog'],new Map([['auth_token','native-token']])) | |
| 21 | await manual.event('distance-input','onChangeText',{text:'3.1'}) | |
| 22 | await manual.event('duration-input','onChangeText',{text:'28:00'}) | |
| 23 | await manual.event('save-activity','onPress') | |
| 24 | const uuid=await waitForRequest(manual,0,r=>r.payload.module==='Crypto' && r.payload.method==='randomUUID') | |
| 25 | answer(manual,uuid,'2b53ece4-8df1-49fa-9e70-752ddc901cf7') | |
| 26 | const post=await waitForRequest(manual,0,r=>r.payload.module==='Network') | |
| 27 | expect(JSON.parse(post.payload.args[0].body).upload_id).toBe('manual:2b53ece4-8df1-49fa-9e70-752ddc901cf7') | |
| 28 | answer(manual,post,{status:503,body:JSON.stringify({message:'Try again later'})}) | |
| 29 | await waitForText(manual,'Try again later') | |
| 30 | const before=requests(manual).length | |
| 31 | await manual.event('save-activity','onPress') | |
| 32 | const again=await waitForRequest(manual,before,r=>r.payload.module==='Network') | |
| 33 | expect(again.payload.args[0].body).toBe(post.payload.args[0].body) | |
| 34 | expect(requests(manual).filter(r=>r.payload.module==='Crypto')).toHaveLength(1) | |
| 35 | }) | |
| 36 | it('clears private manual notes and ignores a save response after an account change',async()=>{ | |
| 37 | const keychain=new Map([['auth_token','native-token']]) | |
| 38 | const manual=await start('ManualActivity',{},['fetch','secureStorage','crypto','haptics','lifecycle','dialog'],keychain) | |
| 39 | await manual.event('distance-input','onChangeText',{text:'3'}) | |
| 40 | await manual.event('duration-input','onChangeText',{text:'30:00'}) | |
| 41 | await manual.event('notes-input','onChangeText',{text:'private route notes'}) | |
| 42 | await manual.event('save-activity','onPress') | |
| 43 | await supplyUUID(manual) | |
| 44 | const post=await waitForRequest(manual,0,r=>r.payload.module==='Network') | |
| 45 | keychain.set('auth_token','other-token') | |
| 46 | await manual.scope.craft.snapshots.set('wildloop-session',{user:{id:13},version:1}) | |
| 47 | answer(manual,post,{status:201,body:JSON.stringify({activity:{id:42,userId:12}})}) | |
| 48 | await Bun.sleep(0) | |
| 49 | expect(nativeNodeProps(manual,'notes-input').value).toBe('') | |
| 50 | expect(requests(manual).some(r=>r.payload.module==='Haptics')).toBe(false) | |
| 51 | expect(manual.sent.some(r=>r.type==='NAVIGATE')).toBe(false) | |
| 52 | }) | |
| 11 | 53 | it('uses app components and sends the real activity payload through the Craft protocol', async () => { |
| 12 | 54 | expect(nativeBundle()).not.toContain('WebView') |
| 13 | 55 | expect(readFileSync(join(ROOT, 'resources/native/ManualActivity.stx'), 'utf8')).toContain('<NativeRunMetric') |
| 14 | const manual = await start('ManualActivity', { userId: 12 }) | |
| 56 | const manual = await openManual({ userId: 12 }) | |
| 15 | 57 | const render = manual.sent.find(message => message.type === 'RENDER').payload.document |
| 16 | 58 | expect(nodesByTestId(render).has('distance-preview')).toBe(true) |
| 17 | 59 | |
| @@ -22,10 +64,8 @@ describe('Wildloop native manual activity flow', () => { | ||
| 22 | 64 | await manual.event('notes-input', 'onChangeText', { text: 'ridge loop' }) |
| 23 | 65 | await manual.event('save-activity', 'onPress') |
| 24 | 66 | |
| 25 | const tokenRequest = requests(manual).at(-1) | |
| 26 | expect(tokenRequest.payload).toMatchObject({ module: 'SecureStorage', method: 'get', args: ['auth_token'] }) | |
| 27 | const requestCount = requests(manual).length | |
| 28 | answer(manual, tokenRequest, 'native-token') | |
| 67 | await supplyUUID(manual) | |
| 68 | const requestCount = 0 | |
| 29 | 69 | const post = await waitForRequest(manual, requestCount, request => request.payload?.module === 'Network') |
| 30 | 70 | const request = post.payload.args[0] |
| 31 | 71 | expect(post.payload.module).toBe('Network') |
| @@ -63,10 +103,10 @@ describe('Wildloop native manual activity flow', () => { | ||
| 63 | 103 | expect(haptic.payload).toMatchObject({ module: 'Haptics', method: 'impact', args: ['light'] }) |
| 64 | 104 | answer(manual, haptic, true) |
| 65 | 105 | await Bun.sleep(0) |
| 66 | expect(manual.sent.at(-1)).toMatchObject({ | |
| 106 | expect(manual.sent).toContainEqual(expect.objectContaining({ | |
| 67 | 107 | type: 'NAVIGATE', |
| 68 | 108 | payload: { screen: 'ActivitySaved', params: expect.objectContaining({ id: 42, userId: 12, distance: 5.13 }) }, |
| 69 | }) | |
| 109 | })) | |
| 70 | 110 | }) |
| 71 | 111 | |
| 72 | 112 | /* |
| @@ -80,18 +120,15 @@ describe('Wildloop native manual activity flow', () => { | ||
| 80 | 120 | * the token is the only identity this screen needs. |
| 81 | 121 | */ |
| 82 | 122 | it('saves on a cold start, with no route params and no user id in the body', async () => { |
| 83 | const manual = await start('ManualActivity') | |
| 123 | const manual = await openManual() | |
| 84 | 124 | |
| 85 | 125 | await manual.event('distance-input', 'onChangeText', { text: '3.1' }) |
| 86 | 126 | await manual.event('duration-input', 'onChangeText', { text: '28:00' }) |
| 87 | 127 | await manual.event('save-activity', 'onPress') |
| 88 | 128 | |
| 89 | const tokenRequest = requests(manual).at(-1) | |
| 90 | expect(tokenRequest.payload, 'the token is read before anything is refused') | |
| 91 | .toMatchObject({ module: 'SecureStorage', method: 'get', args: ['auth_token'] }) | |
| 92 | ||
| 93 | const before = requests(manual).length | |
| 94 | answer(manual, tokenRequest, 'native-token') | |
| 129 | await supplyUUID(manual) | |
| 130 | expect(requests(manual).some(r=>r.payload.module==='SecureStorage' && r.payload.method==='get')).toBe(true) | |
| 131 | const before = 0 | |
| 95 | 132 | const post = await waitForRequest(manual, before, request => request.payload?.module === 'Network') |
| 96 | 133 | const body = JSON.parse(post.payload.args[0].body) |
| 97 | 134 | |
| @@ -115,13 +152,12 @@ describe('Wildloop native manual activity flow', () => { | ||
| 115 | 152 | await saved.event('log-another', 'onPress') |
| 116 | 153 | expect(saved.sent.at(-1)).toMatchObject({ type: 'NAVIGATE_REPLACE', payload: { screen: 'ManualActivity', params: { userId: 12 } } }) |
| 117 | 154 | |
| 118 | const pending = await start('ManualActivity', { userId: 12 }) | |
| 155 | const pending = await openManual({ userId: 12 }) | |
| 119 | 156 | await pending.event('distance-input', 'onChangeText', { text: '3' }) |
| 120 | 157 | await pending.event('duration-input', 'onChangeText', { text: '30:00' }) |
| 121 | 158 | await pending.event('save-activity', 'onPress') |
| 122 | const tokenRequest = requests(pending).at(-1) | |
| 123 | const requestCount = requests(pending).length | |
| 124 | answer(pending, tokenRequest, 'native-token') | |
| 159 | await supplyUUID(pending) | |
| 160 | const requestCount = 0 | |
| 125 | 161 | const post = await waitForRequest(pending, requestCount, request => request.payload?.module === 'Network') |
| 126 | 162 | pending.scope.__stxNativeUnmount() |
| 127 | 163 | expect(pending.sent).toContainEqual(expect.objectContaining({ |
| @@ -129,7 +165,7 @@ describe('Wildloop native manual activity flow', () => { | ||
| 129 | 165 | payload: { version: 1, requestId: post.id, reason: 'screen-closed' }, |
| 130 | 166 | })) |
| 131 | 167 | |
| 132 | const fresh = await start('ManualActivity', { userId: 12 }) | |
| 168 | const fresh = await openManual({ userId: 12 }) | |
| 133 | 169 | const operations = fresh.sent.filter(message => message.type === 'MUTATE').flatMap(message => message.payload.operations) |
| 134 | 170 | expect(operations.some(operation => operation.patch?.children?.includes('3 mi'))).toBe(false) |
| 135 | 171 | }) |
| @@ -3,17 +3,89 @@ import { answer, compileNativeBundle, nativeNodeStyle, nativeNodeProps, requests | ||
| 3 | 3 | |
| 4 | 4 | const checkpoint={userId:7,id:'run-7',startedAt:100000,type:'Hike',visibility:'private',gameMode:'free',targetTerritoryId:null,pauses:[],samples:[],pending:{upload_id:'run:run-7',completed_at:'2026-10-17T12:00:00Z'}} |
| 5 | 5 | function ok(body:unknown,status=200){return {status,headers:{'content-type':'application/json'},body:JSON.stringify(body)}} |
| 6 | async function recover(userId=7) { | |
| 7 | const screen=await start('Record',{},['fetch','secureStorage','storage','geolocation','lifecycle','liveActivity']) | |
| 8 | for(const request of requests(screen).filter(r=>r.payload.module==='SecureStorage')) answer(screen,request,request.payload.args[0]==='auth_token' ? 'record-token' : JSON.stringify({token:'record-token',user:{id:userId}})) | |
| 6 | async function recover(userId=7, keychain=new Map([['auth_token','record-token'],['wildloop-account',JSON.stringify({token:'record-token',user:{id:userId}})]])) { | |
| 7 | const screen=await start('Record',{},['fetch','secureStorage','storage','geolocation','locationRecordingOwnership','lifecycle','liveActivity'],keychain) | |
| 9 | 8 | const stored=await waitForRequest(screen,0,r=>r.payload.module==='Storage' && r.payload.method==='get') |
| 10 | 9 | answer(screen,stored,checkpoint) |
| 11 | const host=await waitForRequest(screen,0,r=>r.payload.method==='getRecordingState') | |
| 12 | answer(screen,host,{active:false,paused:false,locations:[]}) | |
| 13 | 10 | return screen |
| 14 | 11 | } |
| 15 | 12 | describe('native Record checkpoint recovery',()=>{ |
| 16 | 13 | beforeAll(compileNativeBundle,30000) |
| 14 | it('registers a system ID before GPS and retains it through pause, resume and save',async()=>{ | |
| 15 | const screen=await start('Record',{},['fetch','secureStorage','storage','storageCompareAndSet','storageSetIfAbsent','crypto','geolocation','locationRecordingOwnership','locationRecordingIntentIdentity','lifecycle','liveActivity'],new Map([['auth_token','record-token'],['wildloop-account',JSON.stringify({token:'record-token',user:{id:7}})]])) | |
| 16 | const stored=await waitForRequest(screen,0,r=>r.payload.module==='Storage' && r.payload.method==='get') | |
| 17 | answer(screen,stored,null) | |
| 18 | const initialHost=await waitForRequest(screen,0,r=>r.payload.method==='getRecordingState') | |
| 19 | answer(screen,initialHost,{active:false,paused:false}) | |
| 20 | await Bun.sleep(0) | |
| 21 | await screen.event('record-start','onPress') | |
| 22 | const host=await waitForRequest(screen,requests(screen).indexOf(initialHost)+1,r=>r.payload.method==='getRecordingState') | |
| 23 | answer(screen,host,{active:false,paused:false}) | |
| 24 | const uuid=await waitForRequest(screen,0,r=>r.payload.module==='Crypto' && r.payload.method==='randomUUID') | |
| 25 | answer(screen,uuid,'782f0d4a-f626-41bc-8f17-2f67e9a099bf') | |
| 26 | const registration=await waitForRequest(screen,0,r=>r.payload.module==='Storage' && r.payload.method==='setIfAbsent') | |
| 27 | expect(registration.payload.args).toMatchObject(['wildloop-native-recording-v1',{userId:7,id:'782f0d4a-f626-41bc-8f17-2f67e9a099bf',phase:'starting'}]) | |
| 28 | expect(requests(screen).some(r=>r.payload.method==='startRecording')).toBe(false) | |
| 29 | answer(screen,registration,true) | |
| 30 | const nativeStart=await waitForRequest(screen,0,r=>r.payload.method==='startRecording') | |
| 31 | expect(nativeStart.payload.args[0]).toMatchObject({recordingId:'782f0d4a-f626-41bc-8f17-2f67e9a099bf'}) | |
| 32 | const id='782f0d4a-f626-41bc-8f17-2f67e9a099bf' | |
| 33 | const startedAt=Date.now()-30000 | |
| 34 | const locations=[{latitude:35.699,longitude:-82.38,timestamp:startedAt+1000,accuracy:5},{latitude:35.6991,longitude:-82.3801,timestamp:startedAt+2000,accuracy:5}] | |
| 35 | answer(screen,nativeStart,{id,active:true,paused:false,startedAt}) | |
| 36 | const started=await waitForRequest(screen,requests(screen).indexOf(nativeStart)+1,r=>r.payload.method==='compareAndSet') | |
| 37 | expect(started.payload.args[2]).toMatchObject({id,phase:'recording',startedAt}) | |
| 38 | answer(screen,started,true) | |
| 39 | await waitForText(screen,'Recording continues with your phone locked.') | |
| 40 | let before=requests(screen).length | |
| 41 | await screen.event('record-pause','onPress') | |
| 42 | const pause=await waitForRequest(screen,before,r=>r.payload.method==='pauseRecording') | |
| 43 | expect(pause.payload.args[0]).toEqual({expectedId:id}) | |
| 44 | answer(screen,pause,{id,active:true,paused:true,startedAt}) | |
| 45 | const read=await waitForRequest(screen,before,r=>r.payload.method==='readRecording') | |
| 46 | expect(read.payload.args[0]).toEqual({expectedId:id}) | |
| 47 | answer(screen,read,locations) | |
| 48 | const paused=await waitForRequest(screen,before,r=>r.payload.method==='compareAndSet') | |
| 49 | expect(paused.payload.args[2].pauses).toHaveLength(1) | |
| 50 | answer(screen,paused,true) | |
| 51 | await Bun.sleep(0) | |
| 52 | before=requests(screen).length | |
| 53 | await screen.event('record-pause','onPress') | |
| 54 | const resume=await waitForRequest(screen,before,r=>r.payload.method==='resumeRecording') | |
| 55 | expect(resume.payload.args[0]).toEqual({expectedId:id}) | |
| 56 | answer(screen,resume,{id,active:true,paused:false,startedAt}) | |
| 57 | const resumed=await waitForRequest(screen,before,r=>r.payload.method==='compareAndSet') | |
| 58 | expect(resumed.payload.args[2].pauses[0].end).toBeNumber() | |
| 59 | answer(screen,resumed,true) | |
| 60 | await Bun.sleep(0) | |
| 61 | before=requests(screen).length | |
| 62 | await screen.event('record-finish','onPress') | |
| 63 | const stop=await waitForRequest(screen,before,r=>r.payload.method==='stopRecording') | |
| 64 | expect(stop.payload.args[0]).toEqual({expectedId:id}) | |
| 65 | answer(screen,stop,{id,active:false,paused:false,startedAt,locations}) | |
| 66 | const stopped=await waitForRequest(screen,before,r=>r.payload.method==='compareAndSet') | |
| 67 | expect(stopped.payload.args[2]).toMatchObject({id,phase:'stopped'}) | |
| 68 | answer(screen,stopped,true) | |
| 69 | const pending=await waitForRequest(screen,requests(screen).indexOf(stopped)+1,r=>r.payload.method==='compareAndSet') | |
| 70 | expect(pending.payload.args[2].pending.upload_id).toBe(`run:${id}`) | |
| 71 | expect(requests(screen).some(r=>r.payload.method==='acknowledgeRecording')).toBe(false) | |
| 72 | answer(screen,pending,true) | |
| 73 | const me=await waitForRequest(screen,before,r=>r.payload.module==='Network') | |
| 74 | answer(screen,me,ok({user:{id:7}})) | |
| 75 | const post=await waitForRequest(screen,requests(screen).indexOf(me)+1,r=>r.payload.module==='Network') | |
| 76 | expect(JSON.parse(post.payload.args[0].body).upload_id).toBe(`run:${id}`) | |
| 77 | answer(screen,post,ok({activity:{id:99,captureEligible:false}},201)) | |
| 78 | const ack=await waitForRequest(screen,before,r=>r.payload.method==='acknowledgeRecording') | |
| 79 | expect(ack.payload.args[0]).toEqual({expectedId:id}) | |
| 80 | answer(screen,ack,true) | |
| 81 | const clear=await waitForRequest(screen,requests(screen).indexOf(pending)+1,r=>r.payload.method==='compareAndSet') | |
| 82 | expect(clear.payload.args[1]).toEqual(pending.payload.args[2]) | |
| 83 | expect(clear.payload.args[2]).toBeNull() | |
| 84 | answer(screen,clear,true) | |
| 85 | await Bun.sleep(0) | |
| 86 | expect(screen.sent.some(r=>r.type==='NAVIGATE_OPEN' && JSON.stringify(r.payload).includes('/activity/99'))).toBe(true) | |
| 87 | expect(requests(screen).some(r=>r.payload.method==='set' || r.payload.method==='remove')).toBe(false) | |
| 88 | }) | |
| 17 | 89 | it('recovers a finished upload offline without starting another GPS recording',async()=>{ |
| 18 | 90 | const screen=await recover() |
| 19 | 91 | await waitForText(screen,'Your finished recording is saved on this device. Retry the upload when connected.') |
| @@ -24,17 +96,93 @@ describe('native Record checkpoint recovery',()=>{ | ||
| 24 | 96 | expect(requests(screen).some(r=>r.payload.module==='Network')).toBe(false) |
| 25 | 97 | expect(shownText(screen).some(text=>text.includes('moving ·'))).toBe(true) |
| 26 | 98 | }) |
| 99 | it('retains an existing checkpoint when native registration loses a race',async()=>{ | |
| 100 | const screen=await start('Record',{},['fetch','secureStorage','storage','storageCompareAndSet','storageSetIfAbsent','crypto','geolocation','locationRecordingOwnership','locationRecordingIntentIdentity','lifecycle'],new Map([['auth_token','record-token'],['wildloop-account',JSON.stringify({token:'record-token',user:{id:7}})]])) | |
| 101 | const stored=await waitForRequest(screen,0,r=>r.payload.module==='Storage' && r.payload.method==='get') | |
| 102 | answer(screen,stored,null) | |
| 103 | const initial=await waitForRequest(screen,0,r=>r.payload.method==='getRecordingState') | |
| 104 | answer(screen,initial,{active:false,paused:false}) | |
| 105 | await Bun.sleep(0) | |
| 106 | await screen.event('record-start','onPress') | |
| 107 | const host=await waitForRequest(screen,requests(screen).indexOf(initial)+1,r=>r.payload.method==='getRecordingState') | |
| 108 | answer(screen,host,{active:false,paused:false}) | |
| 109 | const uuid=await waitForRequest(screen,0,r=>r.payload.module==='Crypto') | |
| 110 | answer(screen,uuid,'803798f7-733a-4692-9ca5-6dca46acc6e2') | |
| 111 | const create=await waitForRequest(screen,0,r=>r.payload.method==='setIfAbsent') | |
| 112 | answer(screen,create,false) | |
| 113 | const replace=await waitForRequest(screen,0,r=>r.payload.method==='compareAndSet') | |
| 114 | expect(replace.payload.args[1]).toBeNull() | |
| 115 | answer(screen,replace,false) | |
| 116 | await waitForText(screen,'The saved recording changed on this device. Pull down to recover it before trying again. Your track has been retained.') | |
| 117 | expect(requests(screen).some(r=>r.payload.method==='startRecording' || r.payload.method==='set' || r.payload.method==='remove')).toBe(false) | |
| 118 | }) | |
| 119 | it('acknowledges only the completed owner and never clears a replaced checkpoint',async()=>{ | |
| 120 | const saved={...checkpoint,phase:'stopped',endedAt:160000} | |
| 121 | const screen=await start('Record',{},['fetch','secureStorage','storage','storageCompareAndSet','geolocation','locationRecordingOwnership','lifecycle'],new Map([['auth_token','record-token'],['wildloop-account',JSON.stringify({token:'record-token',user:{id:7}})]])) | |
| 122 | const stored=await waitForRequest(screen,0,r=>r.payload.module==='Storage' && r.payload.method==='get') | |
| 123 | answer(screen,stored,saved) | |
| 124 | await waitForText(screen,'Your finished recording is saved on this device. Retry the upload when connected.') | |
| 125 | await screen.event('record-retry','onPress') | |
| 126 | const me=await waitForRequest(screen,0,r=>r.payload.module==='Network') | |
| 127 | answer(screen,me,ok({user:{id:7}})) | |
| 128 | const post=await waitForRequest(screen,requests(screen).indexOf(me)+1,r=>r.payload.module==='Network') | |
| 129 | expect(requests(screen).some(r=>r.payload.method==='acknowledgeRecording')).toBe(false) | |
| 130 | answer(screen,post,ok({activity:{id:99,captureEligible:false}},201)) | |
| 131 | const ack=await waitForRequest(screen,0,r=>r.payload.method==='acknowledgeRecording') | |
| 132 | expect(ack.payload.args[0]).toEqual({expectedId:'run-7'}) | |
| 133 | expect(requests(screen).some(r=>r.payload.method==='compareAndSet')).toBe(false) | |
| 134 | answer(screen,ack,true) | |
| 135 | const clear=await waitForRequest(screen,0,r=>r.payload.method==='compareAndSet') | |
| 136 | expect(clear.payload.args).toEqual(['wildloop-native-recording-v1',saved,null]) | |
| 137 | answer(screen,clear,false) | |
| 138 | await waitForText(screen,'The saved recording changed on this device. Pull down to recover it before trying again. Your track has been retained.') | |
| 139 | expect(requests(screen).some(r=>r.payload.method==='remove')).toBe(false) | |
| 140 | expect(screen.sent.some(r=>r.type==='NAVIGATE')).toBe(false) | |
| 141 | }) | |
| 27 | 142 | it('does not reveal or upload another account’s saved track',async()=>{ |
| 28 | 143 | const screen=await recover(8) |
| 29 | 144 | await waitForText(screen,'Log in as the account that started the saved recording before resuming or uploading it.') |
| 30 | 145 | expect(requests(screen).some(r=>r.payload.module==='Network')).toBe(false) |
| 31 | 146 | expect(requests(screen).some(r=>r.payload.method==='startRecording')).toBe(false) |
| 147 | expect(requests(screen).some(r=>r.payload.method==='getRecordingState' || r.payload.method==='readRecording')).toBe(false) | |
| 148 | }) | |
| 149 | it('checks the saved identity atomically before recovering the host recording',async()=>{ | |
| 150 | const screen=await start('Record',{},['fetch','secureStorage','storage','geolocation','locationRecordingOwnership','lifecycle','liveActivity'],new Map([['auth_token','record-token'],['wildloop-account',JSON.stringify({token:'record-token',user:{id:7}})]])) | |
| 151 | const stored=await waitForRequest(screen,0,r=>r.payload.module==='Storage' && r.payload.method==='get') | |
| 152 | answer(screen,stored,{...checkpoint,pending:undefined}) | |
| 153 | const host=await waitForRequest(screen,0,r=>r.payload.method==='getRecordingState') | |
| 154 | expect(host.payload.args[0]).toMatchObject({expectedId:'run-7'}) | |
| 155 | answer(screen,host,{active:true,paused:false,id:'run-7',startedAt:100000}) | |
| 156 | const read=await waitForRequest(screen,requests(screen).indexOf(host)+1,r=>r.payload.method==='readRecording') | |
| 157 | expect(read.payload.args[0]).toMatchObject({expectedId:'run-7'}) | |
| 158 | }) | |
| 159 | it('recovers a registered start whose native acknowledgement was interrupted',async()=>{ | |
| 160 | const screen=await start('Record',{},['fetch','secureStorage','storage','storageCompareAndSet','geolocation','locationRecordingOwnership','lifecycle'],new Map([['auth_token','record-token'],['wildloop-account',JSON.stringify({token:'record-token',user:{id:7}})]])) | |
| 161 | const intent={...checkpoint,phase:'starting',pending:undefined} | |
| 162 | const stored=await waitForRequest(screen,0,r=>r.payload.module==='Storage' && r.payload.method==='get') | |
| 163 | answer(screen,stored,intent) | |
| 164 | const host=await waitForRequest(screen,0,r=>r.payload.method==='getRecordingState') | |
| 165 | expect(host.payload.args[0]).toEqual({expectedId:'run-7'}) | |
| 166 | answer(screen,host,{active:true,paused:false,id:'run-7',startedAt:100000}) | |
| 167 | const read=await waitForRequest(screen,0,r=>r.payload.method==='readRecording') | |
| 168 | answer(screen,read,[]) | |
| 169 | const durable=await waitForRequest(screen,0,r=>r.payload.method==='compareAndSet') | |
| 170 | expect(durable.payload.args[1]).toMatchObject({id:'run-7',phase:'starting'}) | |
| 171 | expect(durable.payload.args[2]).toMatchObject({id:'run-7',phase:'recording'}) | |
| 172 | expect(requests(screen).some(r=>r.payload.method==='startRecording')).toBe(false) | |
| 173 | }) | |
| 174 | it('does not read GPS when a legacy checkpoint has no verifiable host identity',async()=>{ | |
| 175 | const screen=await start('Record',{},['fetch','secureStorage','storage','geolocation','locationRecordingOwnership','lifecycle','liveActivity'],new Map([['auth_token','record-token'],['wildloop-account',JSON.stringify({token:'record-token',user:{id:7}})]])) | |
| 176 | const stored=await waitForRequest(screen,0,r=>r.payload.module==='Storage' && r.payload.method==='get') | |
| 177 | answer(screen,stored,{...checkpoint,id:null,pending:undefined}) | |
| 178 | await Bun.sleep(0) | |
| 179 | expect(requests(screen).some(r=>r.payload.method==='getRecordingState' || r.payload.method==='readRecording')).toBe(false) | |
| 180 | expect(requests(screen).some(r=>r.payload.method==='startRecording')).toBe(false) | |
| 32 | 181 | }) |
| 33 | 182 | it('retains a finished track when uploading fails and retries the same identity',async()=>{ |
| 34 | 183 | const screen=await recover() |
| 35 | 184 | await waitForText(screen,'Your finished recording is saved on this device. Retry the upload when connected.') |
| 36 | 185 | await screen.event('record-retry','onPress') |
| 37 | answer(screen,await waitForRequest(screen,2,r=>r.payload.module==='SecureStorage' && r.payload.args[0]==='auth_token'),'record-token') | |
| 38 | 186 | const me=await waitForRequest(screen,0,r=>r.payload.module==='Network') |
| 39 | 187 | answer(screen,me,ok({user:{id:7}})) |
| 40 | 188 | const post=await waitForRequest(screen,requests(screen).indexOf(me)+1,r=>r.payload.module==='Network') |
| @@ -46,10 +194,23 @@ describe('native Record checkpoint recovery',()=>{ | ||
| 46 | 194 | expect(nativeNodeProps(screen,'record-retry').disabled).toBe(false) |
| 47 | 195 | const before=requests(screen).length |
| 48 | 196 | await screen.event('record-retry','onPress') |
| 49 | answer(screen,await waitForRequest(screen,before,r=>r.payload.module==='SecureStorage' && r.payload.args[0]==='auth_token'),'record-token') | |
| 50 | 197 | const againMe=await waitForRequest(screen,before,r=>r.payload.module==='Network') |
| 51 | 198 | answer(screen,againMe,ok({user:{id:7}})) |
| 52 | 199 | const again=await waitForRequest(screen,requests(screen).indexOf(againMe)+1,r=>r.payload.module==='Network') |
| 53 | 200 | expect(again.payload.args[0].body).toBe(post.payload.args[0].body) |
| 54 | 201 | }) |
| 202 | it('retains the pending track when the bearer changes while its owner is checked',async()=>{ | |
| 203 | const keychain=new Map([['auth_token','record-token'],['wildloop-account',JSON.stringify({token:'record-token',user:{id:7}})]]) | |
| 204 | const screen=await recover(7,keychain) | |
| 205 | await waitForText(screen,'Your finished recording is saved on this device. Retry the upload when connected.') | |
| 206 | const before=requests(screen).length | |
| 207 | await screen.event('record-retry','onPress') | |
| 208 | const me=await waitForRequest(screen,before,r=>r.payload.module==='Network') | |
| 209 | keychain.set('auth_token','replacement-token') | |
| 210 | answer(screen,me,ok({user:{id:7}})) | |
| 211 | await Bun.sleep(0) | |
| 212 | expect(requests(screen).slice(before).filter(r=>r.payload.module==='Network' && r.payload.args[0].method==='POST')).toHaveLength(0) | |
| 213 | expect(requests(screen).some(r=>r.payload.module==='Storage' && r.payload.method==='remove')).toBe(false) | |
| 214 | await waitForText(screen,'Your account changed. Please try again.') | |
| 215 | }) | |
| 55 | 216 | }) |
| @@ -0,0 +1,20 @@ | ||
| 1 | import { describe, expect, it } from 'bun:test' | |
| 2 | import { assertNativeReviewPersistence } from '../../scripts/run-native-trail-android-e2e' | |
| 3 | ||
| 4 | describe('native review acceptance persistence checks', () => { | |
| 5 | it('requires the edited rating and text on the original review', () => { | |
| 6 | const created = { id: 7, content: 'First visit.', rating: 5 } | |
| 7 | const id = assertNativeReviewPersistence([created], 'create', created.content) | |
| 8 | expect(id).toBe(7) | |
| 9 | expect(() => assertNativeReviewPersistence([created], 'edit', 'Return visit.', id)).toThrow('expected review') | |
| 10 | expect(() => assertNativeReviewPersistence([{ ...created, id: 8, content: 'Return visit.', rating: 4 }], 'edit', 'Return visit.', id)).toThrow('second review') | |
| 11 | expect(assertNativeReviewPersistence([{ ...created, content: 'Return visit.', rating: 4 }], 'edit', 'Return visit.', id)).toBe(id) | |
| 12 | }) | |
| 13 | ||
| 14 | it('rejects duplicate creates and a review left behind after deletion', () => { | |
| 15 | const row = { id: 7, content: 'First visit.', rating: 5 } | |
| 16 | expect(() => assertNativeReviewPersistence([row, { ...row, id: 8 }], 'create', row.content)).toThrow('expected review') | |
| 17 | expect(() => assertNativeReviewPersistence([row], 'delete', row.content)).toThrow('did not remove') | |
| 18 | expect(assertNativeReviewPersistence([], 'delete', row.content)).toBeUndefined() | |
| 19 | }) | |
| 20 | }) | |
| @@ -5,13 +5,12 @@ const LINE = [[37.7, -119.5], [37.71, -119.51], [37.72, -119.5]] | ||
| 5 | 5 | const TRAIL = { id: 7, name: 'Native Ridge Loop', latitude: 37.75, longitude: -119.55, distance: 3, elevation: 600, location: 'Yosemite, CA', surface: 'dirt;rock', geometry: JSON.stringify(LINE) } |
| 6 | 6 | const OWN = { id: 5, userId: 12, userName: 'Tester', rating: 4, content: 'A quiet loop with great views.', visitDate: '2026-10-15', conditions: 'dry', photos: '["https://wildloop.org/photo.jpg"]' } |
| 7 | 7 | function ok(body: unknown) { return { status: 200, headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) } } |
| 8 | async function open(signedIn = true, geometry: string | null = TRAIL.geometry, reviewRows: any[] = signedIn ? [OWN] : [], personalReview: any = reviewRows.find(row=>row.userId===12) ?? null, personalStatus = 200) { | |
| 9 | const screen = await start('TrailDetail', { id: '7' }, ['fetch', 'secureStorage', 'haptics', 'lifecycle', 'dialog', 'share', 'linking', 'files']) | |
| 10 | answer(screen, await waitForRequest(screen, 0, r => r.payload?.module === 'SecureStorage'), signedIn ? 'test_token' : null) | |
| 8 | async function open(signedIn = true, geometry: string | null = TRAIL.geometry, reviewRows: any[] = signedIn ? [OWN] : [], personalReview: any = reviewRows.find(row=>row.userId===12) ?? null, personalStatus = 200, keychain = new Map(signedIn ? [['auth_token', 'test_token']] : [])) { | |
| 9 | const screen = await start('TrailDetail', { id: '7' }, ['fetch', 'secureStorage', 'haptics', 'lifecycle', 'dialog', 'share', 'linking', 'files'], keychain) | |
| 11 | 10 | answer(screen, await waitForRequest(screen, 0, r => r.payload?.module === 'Network'), ok({ success: true, trail: { ...TRAIL, geometry } })) |
| 12 | 11 | const review = await waitForRequest(screen, 0, r => r.payload?.args?.[0]?.url?.endsWith('/reviews')) |
| 13 | 12 | answer(screen, review, ok({ reviews: reviewRows })) |
| 14 | answer(screen, await waitForRequest(screen, 0, r => r.payload?.args?.[0]?.url?.endsWith('/forecast')), ok({ current: { temperature: 68, label: 'Clear' }, days: [{ date: '2026-10-17', low: 50, high: 70, label: 'Clear' }], attribution: { label: 'MET Norway', url: 'https://api.met.no' } })) | |
| 13 | answer(screen, await waitForRequest(screen, 0, r => r.payload?.args?.[0]?.url?.endsWith('/forecast')), ok({ current: { temperature: 68, label: 'Clear' }, days: [{ date: '2026-10-17', low: 50, high: 70, label: 'Clear', windMph: 5, precipitationMm: 2.54 }], attribution: { label: 'MET Norway', url: 'https://api.met.no' } })) | |
| 15 | 14 | answer(screen, await waitForRequest(screen, 0, r => r.payload?.args?.[0]?.url?.endsWith('/profile')), ok({ points: geometry ? [{ mi: 0, ft: 4000, lat: 37.7, lng: -119.5 }, { mi: 3, ft: 4500, lat: 37.72, lng: -119.5 }] : [] })) |
| 16 | 15 | if (signedIn) answer(screen, await waitForRequest(screen, 0, r => r.payload?.args?.[0]?.url?.endsWith('/viewer')), {...ok({ userId:12, saved:false, review:personalReview }),status:personalStatus}) |
| 17 | 16 | await waitForVisibility(screen, 'trail-detail-loading', 'none') |
| @@ -30,7 +29,10 @@ describe('native trail details', () => { | ||
| 30 | 29 | expect(nativeNodeProps(screen,'trail-write-review').title).toBe('Edit your review') |
| 31 | 30 | expect(shownText(screen)).toContain('Natural') |
| 32 | 31 | expect(shownText(screen)).toContain('Rock') |
| 33 | expect(shownText(screen)).toContain('68°F, Clear') | |
| 32 | expect(shownText(screen)).toContain('68°F') | |
| 33 | expect(shownText(screen)).toContain('Clear') | |
| 34 | expect(shownText(screen)).toContain('50°–70°F') | |
| 35 | expect(shownText(screen)).toContain('5 mph wind · 0.10 in precipitation') | |
| 34 | 36 | expect(shownText(screen)).toContain('3.0 mi · 4000–4500 ft') |
| 35 | 37 | expect(nativeNodeProps(screen,'trail-native-map')).toMatchObject({scrollEnabled:false,zoomEnabled:false,rotateEnabled:false}) |
| 36 | 38 | for (const id of ['trail-save','trail-directions','trail-share']) expect(nativeNodeStyle(screen,id).flexGrow).toBe(1) |
| @@ -42,7 +44,7 @@ describe('native trail details', () => { | ||
| 42 | 44 | await screen.event('trail-profile-toggle','onPress') |
| 43 | 45 | expect(nativeNodeProps(screen,'trail-profile-toggle').accessibilityValue).toBe('Elevation profile collapsed') |
| 44 | 46 | expect(nativeNodeProps(screen,'trail-native-map').edgePadding.bottom).toBe(88) |
| 45 | await screen.event('trail-map-preview-expand','onPress') | |
| 47 | await screen.event('trail-map-fullscreen','onPress') | |
| 46 | 48 | expect(screen.sent.some(message=>message.type==='NAVIGATE_OPEN' && message.payload.path==='/trail/7/map')).toBe(true) |
| 47 | 49 | }) |
| 48 | 50 | it('changes map layer and camera through native bindings', async () => { |
| @@ -180,6 +182,59 @@ describe('native trail details', () => { | ||
| 180 | 182 | answer(screen,await waitForRequest(screen,before,r=>r.payload?.args?.[0]?.url?.endsWith('/viewer')),ok({userId:12,saved:false,review:null})) |
| 181 | 183 | await waitForText(screen, 'Your review has been deleted.') |
| 182 | 184 | }) |
| 185 | it('does not delete another account’s review after an old confirmation is accepted', async () => { | |
| 186 | const keychain = new Map([['auth_token', 'test_token']]) | |
| 187 | const screen = await open(true, TRAIL.geometry, [OWN], OWN, 200, keychain) | |
| 188 | const before = requests(screen).length | |
| 189 | await screen.event('trail-delete-review', 'onPress') | |
| 190 | const confirm = await waitForRequest(screen, before, r => r.payload.module === 'Dialog') | |
| 191 | keychain.set('auth_token', 'replacement-token') | |
| 192 | await screen.scope.craft.snapshots.set('wildloop-session', {user:{id:13},version:2}) | |
| 193 | answer(screen, confirm, true) | |
| 194 | await Bun.sleep(0) | |
| 195 | expect(requests(screen).slice(before).filter(r => r.payload.module === 'Network' && r.payload.args[0].method === 'DELETE')).toHaveLength(0) | |
| 196 | }) | |
| 197 | it('rejects a stale review draft before a changed bearer’s snapshot arrives', async () => { | |
| 198 | const keychain = new Map([['auth_token', 'test_token']]) | |
| 199 | const screen = await open(true, TRAIL.geometry, [OWN], OWN, 200, keychain) | |
| 200 | await screen.event('trail-write-review', 'onPress') | |
| 201 | await screen.event('trail-review-content', 'onChangeText', {text:'This draft belongs to the original account.'}) | |
| 202 | keychain.set('auth_token', 'replacement-token') | |
| 203 | const before = requests(screen).length | |
| 204 | await screen.event('trail-review-submit', 'onPress') | |
| 205 | expect(requests(screen).slice(before).filter(r => r.payload.module === 'Network' && r.payload.args[0].method === 'POST')).toHaveLength(0) | |
| 206 | await waitForText(screen, 'Your account changed. Please try again.') | |
| 207 | }) | |
| 208 | it('clears a private composer draft when the native account changes', async () => { | |
| 209 | const screen = await open() | |
| 210 | await screen.event('trail-write-review', 'onPress') | |
| 211 | await screen.event('trail-review-content', 'onChangeText', {text:'A private draft for the original account.'}) | |
| 212 | await screen.scope.craft.snapshots.set('wildloop-session', {user:{id:13},version:2}) | |
| 213 | await Bun.sleep(0) | |
| 214 | expect(nativeNodeProps(screen, 'trail-review-composer').visible).toBe(false) | |
| 215 | expect(nativeNodeProps(screen, 'trail-review-content').value).toBe('') | |
| 216 | }) | |
| 217 | it('does not save a trail using stale account credentials', async () => { | |
| 218 | const keychain = new Map([['auth_token', 'test_token']]) | |
| 219 | const screen = await open(true, TRAIL.geometry, [OWN], OWN, 200, keychain) | |
| 220 | keychain.set('auth_token', 'replacement-token') | |
| 221 | const before = requests(screen).length | |
| 222 | await screen.event('trail-save', 'onPress') | |
| 223 | expect(requests(screen).slice(before).filter(r => r.payload.module === 'Network' && ['PUT','DELETE'].includes(r.payload.args[0].method))).toHaveLength(0) | |
| 224 | await waitForText(screen, 'Your account changed. Please try again.') | |
| 225 | }) | |
| 226 | it('ignores a photo-picker result from the previous account', async () => { | |
| 227 | const screen = await open() | |
| 228 | await screen.event('trail-write-review', 'onPress') | |
| 229 | const before = requests(screen).length | |
| 230 | await screen.event('trail-review-add-photo', 'onPress') | |
| 231 | const picker = await waitForRequest(screen, before, r => r.payload.module === 'Files') | |
| 232 | await screen.scope.craft.snapshots.set('wildloop-session', {user:{id:13},version:2}) | |
| 233 | const beforeResponse = screen.sent.length | |
| 234 | answer(screen, picker, {path:'/tmp/original-account-photo.jpg',name:'original-account-photo.jpg',type:'image/jpeg',size:20000}) | |
| 235 | await Bun.sleep(0) | |
| 236 | expect(JSON.stringify(screen.sent.slice(beforeResponse))).not.toContain('original-account-photo.jpg') | |
| 237 | }) | |
| 183 | 238 | it('shows the trailhead for trails without recorded geometry', async () => { |
| 184 | 239 | const screen = await open(false, null) |
| 185 | 240 | expect(shownText(screen)).toContain('No recorded route line. The map shows the trailhead.') |
| @@ -79,6 +79,7 @@ | ||
| 79 | 79 | "scripts/test-recording-api.ts", |
| 80 | 80 | "scripts/start-recording-qa.ts", |
| 81 | 81 | "scripts/run-native-trail-e2e.ts", |
| 82 | "scripts/run-native-trail-android-e2e.ts", | |
| 82 | 83 | "tests/native/**/*.ts", |
| 83 | 84 | "tests/unit/account-deletion.test.ts", |
| 84 | 85 | "tests/unit/battle-rows.test.ts", |