@shopify/shop-minis-react 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_virtual/index10.js +2 -2
- package/dist/_virtual/index4.js +2 -3
- package/dist/_virtual/index4.js.map +1 -1
- package/dist/_virtual/index7.js +3 -2
- package/dist/_virtual/index7.js.map +1 -1
- package/dist/_virtual/index9.js +2 -2
- package/dist/components/atoms/tracking-pixel.js +1 -1
- package/dist/hooks/util/useRequestPermissions.js +12 -0
- package/dist/hooks/util/useRequestPermissions.js.map +1 -0
- package/dist/index.js +222 -220
- package/dist/index.js.map +1 -1
- package/dist/mocks.js +25 -20
- package/dist/mocks.js.map +1 -1
- package/dist/providers/ImagePickerProvider.js +59 -49
- package/dist/providers/ImagePickerProvider.js.map +1 -1
- package/dist/shop-minis-platform/src/types/content.js +3 -2
- package/dist/shop-minis-platform/src/types/content.js.map +1 -1
- package/dist/shop-minis-react/node_modules/.pnpm/@radix-ui_react-use-is-hydrated@0.1.0_@types_react@19.1.6_react@19.1.0/node_modules/@radix-ui/react-use-is-hydrated/dist/index.js +1 -1
- package/dist/shop-minis-react/node_modules/.pnpm/@xmldom_xmldom@0.8.10/node_modules/@xmldom/xmldom/lib/index.js +1 -1
- package/dist/shop-minis-react/node_modules/.pnpm/querystringify@2.2.0/node_modules/querystringify/index.js +1 -1
- package/dist/shop-minis-react/node_modules/.pnpm/react-intersection-observer@9.13.1_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/react-intersection-observer/dist/index.js.map +1 -0
- package/dist/shop-minis-react/node_modules/.pnpm/use-sync-external-store@1.5.0_react@19.1.0/node_modules/use-sync-external-store/shim/index.js +1 -1
- package/package.json +9 -8
- package/src/components/index.ts +1 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/util/useRequestPermissions.ts +24 -0
- package/src/mocks.ts +10 -1
- package/src/providers/ImagePickerProvider.test.tsx +329 -4
- package/src/providers/ImagePickerProvider.tsx +42 -4
- package/dist/shop-minis-platform/src/types/permissions.js +0 -15
- package/dist/shop-minis-platform/src/types/permissions.js.map +0 -1
- package/dist/shop-minis-react/node_modules/.pnpm/react-intersection-observer@9.16.0_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/react-intersection-observer/dist/index.js.map +0 -1
- /package/dist/shop-minis-react/node_modules/.pnpm/{react-intersection-observer@9.16.0_react-dom@19.1.0_react@19.1.0__react@19.1.0 → react-intersection-observer@9.13.1_react-dom@19.1.0_react@19.1.0__react@19.1.0}/node_modules/react-intersection-observer/dist/index.js +0 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RequestPermissionParams,
|
|
3
|
+
RequestPermissionResponse,
|
|
4
|
+
} from '@shopify/shop-minis-platform/actions'
|
|
5
|
+
|
|
6
|
+
import {useHandleAction} from '../../internal/useHandleAction'
|
|
7
|
+
import {useShopActions} from '../../internal/useShopActions'
|
|
8
|
+
|
|
9
|
+
interface UseRequestPermissionsReturns {
|
|
10
|
+
/**
|
|
11
|
+
* Request native permissions from the user
|
|
12
|
+
*/
|
|
13
|
+
requestPermission: (
|
|
14
|
+
params: RequestPermissionParams
|
|
15
|
+
) => Promise<RequestPermissionResponse>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const useRequestPermissions = (): UseRequestPermissionsReturns => {
|
|
19
|
+
const {requestPermission} = useShopActions()
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
requestPermission: useHandleAction(requestPermission),
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/mocks.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Product,
|
|
3
|
+
Gender,
|
|
4
|
+
UserState,
|
|
5
|
+
MinisContentStatus,
|
|
6
|
+
} from '@shopify/shop-minis-platform'
|
|
2
7
|
import {ShopActions} from '@shopify/shop-minis-platform/actions'
|
|
3
8
|
|
|
4
9
|
// Helper functions for common data structures
|
|
@@ -440,6 +445,7 @@ export function makeMockActions(): ShopActions {
|
|
|
440
445
|
},
|
|
441
446
|
title: 'Mock Content',
|
|
442
447
|
visibility: ['DISCOVERABLE'],
|
|
448
|
+
status: MinisContentStatus.READY,
|
|
443
449
|
},
|
|
444
450
|
],
|
|
445
451
|
},
|
|
@@ -451,6 +457,9 @@ export function makeMockActions(): ShopActions {
|
|
|
451
457
|
},
|
|
452
458
|
},
|
|
453
459
|
navigateToCart: undefined,
|
|
460
|
+
requestPermission: {
|
|
461
|
+
granted: true,
|
|
462
|
+
},
|
|
454
463
|
} as const
|
|
455
464
|
|
|
456
465
|
const mock: Partial<ShopActions> = {}
|
|
@@ -5,13 +5,30 @@ import {
|
|
|
5
5
|
renderHook,
|
|
6
6
|
act,
|
|
7
7
|
} from '@testing-library/react'
|
|
8
|
-
import {describe, expect, it, vi, beforeEach} from 'vitest'
|
|
8
|
+
import {describe, expect, it, vi, beforeEach, afterEach} from 'vitest'
|
|
9
9
|
|
|
10
10
|
import {ImagePickerProvider, useImagePickerContext} from './ImagePickerProvider'
|
|
11
11
|
|
|
12
|
+
// Mock useRequestPermissions hook
|
|
13
|
+
const mockRequestPermission = vi.fn()
|
|
14
|
+
vi.mock('../hooks/util/useRequestPermissions', () => ({
|
|
15
|
+
useRequestPermissions: () => ({
|
|
16
|
+
requestPermission: mockRequestPermission,
|
|
17
|
+
}),
|
|
18
|
+
}))
|
|
19
|
+
|
|
12
20
|
describe('ImagePickerProvider', () => {
|
|
21
|
+
const originalMinisParams = window.minisParams
|
|
22
|
+
|
|
13
23
|
beforeEach(() => {
|
|
14
24
|
vi.clearAllMocks()
|
|
25
|
+
// Default to granting permission
|
|
26
|
+
mockRequestPermission.mockResolvedValue({granted: true})
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
// Restore original minisParams
|
|
31
|
+
window.minisParams = originalMinisParams
|
|
15
32
|
})
|
|
16
33
|
|
|
17
34
|
describe('Context', () => {
|
|
@@ -30,7 +47,7 @@ describe('ImagePickerProvider', () => {
|
|
|
30
47
|
|
|
31
48
|
it('provides context value when used within provider', () => {
|
|
32
49
|
const {result} = renderHook(() => useImagePickerContext(), {
|
|
33
|
-
wrapper: ({children}) => (
|
|
50
|
+
wrapper: ({children}: {children: React.ReactNode}) => (
|
|
34
51
|
<ImagePickerProvider>{children}</ImagePickerProvider>
|
|
35
52
|
),
|
|
36
53
|
})
|
|
@@ -72,7 +89,7 @@ describe('ImagePickerProvider', () => {
|
|
|
72
89
|
})
|
|
73
90
|
|
|
74
91
|
describe('openGallery', () => {
|
|
75
|
-
it('triggers gallery input click
|
|
92
|
+
it('triggers gallery input click directly on non-Android platforms', async () => {
|
|
76
93
|
const TestComponent = () => {
|
|
77
94
|
const {openGallery} = useImagePickerContext()
|
|
78
95
|
|
|
@@ -181,10 +198,106 @@ describe('ImagePickerProvider', () => {
|
|
|
181
198
|
expect(cancelMessage?.textContent).toBe('User cancelled file selection')
|
|
182
199
|
})
|
|
183
200
|
})
|
|
201
|
+
|
|
202
|
+
it('requests CAMERA permission on Android before showing picker', async () => {
|
|
203
|
+
window.minisParams = {...window.minisParams, platform: 'android'} as any
|
|
204
|
+
mockRequestPermission.mockResolvedValue({granted: true})
|
|
205
|
+
|
|
206
|
+
const TestComponent = () => {
|
|
207
|
+
const {openGallery} = useImagePickerContext()
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<button
|
|
211
|
+
type="button"
|
|
212
|
+
onClick={() =>
|
|
213
|
+
openGallery().catch(() => {
|
|
214
|
+
// Ignore errors from cleanup
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
>
|
|
218
|
+
Open Gallery
|
|
219
|
+
</button>
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const {container} = render(
|
|
224
|
+
<ImagePickerProvider>
|
|
225
|
+
<TestComponent />
|
|
226
|
+
</ImagePickerProvider>
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
const galleryInput = container.querySelector(
|
|
230
|
+
'input[type="file"]:not([capture])'
|
|
231
|
+
) as HTMLInputElement
|
|
232
|
+
const clickSpy = vi.spyOn(galleryInput, 'click')
|
|
233
|
+
|
|
234
|
+
const button = screen.getByText('Open Gallery')
|
|
235
|
+
fireEvent.click(button)
|
|
236
|
+
|
|
237
|
+
// Wait for permission request to complete
|
|
238
|
+
await vi.waitFor(() => {
|
|
239
|
+
expect(mockRequestPermission).toHaveBeenCalledWith({
|
|
240
|
+
permission: 'CAMERA',
|
|
241
|
+
})
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
// Input should be clicked after permission request
|
|
245
|
+
await vi.waitFor(() => {
|
|
246
|
+
expect(clickSpy).toHaveBeenCalledTimes(1)
|
|
247
|
+
})
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it('still opens picker on Android even if CAMERA permission is denied', async () => {
|
|
251
|
+
window.minisParams = {...window.minisParams, platform: 'android'} as any
|
|
252
|
+
mockRequestPermission.mockRejectedValue(new Error('Permission denied'))
|
|
253
|
+
|
|
254
|
+
const TestComponent = () => {
|
|
255
|
+
const {openGallery} = useImagePickerContext()
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
<button
|
|
259
|
+
type="button"
|
|
260
|
+
onClick={() =>
|
|
261
|
+
openGallery().catch(() => {
|
|
262
|
+
// Ignore errors from cleanup
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
>
|
|
266
|
+
Open Gallery
|
|
267
|
+
</button>
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const {container} = render(
|
|
272
|
+
<ImagePickerProvider>
|
|
273
|
+
<TestComponent />
|
|
274
|
+
</ImagePickerProvider>
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
const galleryInput = container.querySelector(
|
|
278
|
+
'input[type="file"]:not([capture])'
|
|
279
|
+
) as HTMLInputElement
|
|
280
|
+
const clickSpy = vi.spyOn(galleryInput, 'click')
|
|
281
|
+
|
|
282
|
+
const button = screen.getByText('Open Gallery')
|
|
283
|
+
fireEvent.click(button)
|
|
284
|
+
|
|
285
|
+
// Wait for permission request to complete
|
|
286
|
+
await vi.waitFor(() => {
|
|
287
|
+
expect(mockRequestPermission).toHaveBeenCalledWith({
|
|
288
|
+
permission: 'CAMERA',
|
|
289
|
+
})
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
// Input should still be clicked even after permission rejection
|
|
293
|
+
await vi.waitFor(() => {
|
|
294
|
+
expect(clickSpy).toHaveBeenCalledTimes(1)
|
|
295
|
+
})
|
|
296
|
+
})
|
|
184
297
|
})
|
|
185
298
|
|
|
186
299
|
describe('openCamera', () => {
|
|
187
|
-
it('triggers back camera input click
|
|
300
|
+
it('triggers back camera input click directly on non-Android platforms', async () => {
|
|
188
301
|
const TestComponent = () => {
|
|
189
302
|
const {openCamera} = useImagePickerContext()
|
|
190
303
|
|
|
@@ -356,6 +469,218 @@ describe('ImagePickerProvider', () => {
|
|
|
356
469
|
expect(cancelMessage?.textContent).toBe('User cancelled camera')
|
|
357
470
|
})
|
|
358
471
|
})
|
|
472
|
+
|
|
473
|
+
it('requests CAMERA permission on Android and opens camera if granted', async () => {
|
|
474
|
+
window.minisParams = {...window.minisParams, platform: 'android'} as any
|
|
475
|
+
mockRequestPermission.mockResolvedValue({granted: true})
|
|
476
|
+
|
|
477
|
+
const TestComponent = () => {
|
|
478
|
+
const {openCamera} = useImagePickerContext()
|
|
479
|
+
|
|
480
|
+
return (
|
|
481
|
+
<button
|
|
482
|
+
type="button"
|
|
483
|
+
onClick={() =>
|
|
484
|
+
openCamera().catch(() => {
|
|
485
|
+
// Ignore errors from cleanup
|
|
486
|
+
})
|
|
487
|
+
}
|
|
488
|
+
>
|
|
489
|
+
Open Camera
|
|
490
|
+
</button>
|
|
491
|
+
)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const {container} = render(
|
|
495
|
+
<ImagePickerProvider>
|
|
496
|
+
<TestComponent />
|
|
497
|
+
</ImagePickerProvider>
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
const cameraInput = container.querySelector(
|
|
501
|
+
'input[capture="environment"]'
|
|
502
|
+
) as HTMLInputElement
|
|
503
|
+
const clickSpy = vi.spyOn(cameraInput, 'click')
|
|
504
|
+
|
|
505
|
+
const button = screen.getByText('Open Camera')
|
|
506
|
+
fireEvent.click(button)
|
|
507
|
+
|
|
508
|
+
// Wait for permission request to complete
|
|
509
|
+
await vi.waitFor(() => {
|
|
510
|
+
expect(mockRequestPermission).toHaveBeenCalledWith({
|
|
511
|
+
permission: 'CAMERA',
|
|
512
|
+
})
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
// Input should be clicked after permission is granted
|
|
516
|
+
await vi.waitFor(() => {
|
|
517
|
+
expect(clickSpy).toHaveBeenCalledTimes(1)
|
|
518
|
+
})
|
|
519
|
+
})
|
|
520
|
+
|
|
521
|
+
it('rejects with error on Android if CAMERA permission is not granted', async () => {
|
|
522
|
+
window.minisParams = {...window.minisParams, platform: 'android'} as any
|
|
523
|
+
mockRequestPermission.mockResolvedValue({granted: false})
|
|
524
|
+
|
|
525
|
+
const TestComponent = () => {
|
|
526
|
+
const {openCamera} = useImagePickerContext()
|
|
527
|
+
|
|
528
|
+
return (
|
|
529
|
+
<button
|
|
530
|
+
type="button"
|
|
531
|
+
onClick={() =>
|
|
532
|
+
openCamera().catch(error => {
|
|
533
|
+
const span = document.createElement('span')
|
|
534
|
+
span.textContent = error.message
|
|
535
|
+
span.setAttribute('data-testid', 'permission-error')
|
|
536
|
+
document.body.appendChild(span)
|
|
537
|
+
})
|
|
538
|
+
}
|
|
539
|
+
>
|
|
540
|
+
Open Camera
|
|
541
|
+
</button>
|
|
542
|
+
)
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const {container} = render(
|
|
546
|
+
<ImagePickerProvider>
|
|
547
|
+
<TestComponent />
|
|
548
|
+
</ImagePickerProvider>
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
const cameraInput = container.querySelector(
|
|
552
|
+
'input[capture="environment"]'
|
|
553
|
+
) as HTMLInputElement
|
|
554
|
+
const clickSpy = vi.spyOn(cameraInput, 'click')
|
|
555
|
+
|
|
556
|
+
const button = screen.getByText('Open Camera')
|
|
557
|
+
fireEvent.click(button)
|
|
558
|
+
|
|
559
|
+
// Wait for permission request to complete
|
|
560
|
+
await vi.waitFor(() => {
|
|
561
|
+
expect(mockRequestPermission).toHaveBeenCalledWith({
|
|
562
|
+
permission: 'CAMERA',
|
|
563
|
+
})
|
|
564
|
+
})
|
|
565
|
+
|
|
566
|
+
// Input should NOT be clicked when permission is denied
|
|
567
|
+
expect(clickSpy).not.toHaveBeenCalled()
|
|
568
|
+
|
|
569
|
+
// Should show error message
|
|
570
|
+
await vi.waitFor(() => {
|
|
571
|
+
const errorMessage = document.querySelector(
|
|
572
|
+
'[data-testid="permission-error"]'
|
|
573
|
+
)
|
|
574
|
+
expect(errorMessage?.textContent).toBe('Camera permission not granted')
|
|
575
|
+
})
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
it('rejects with error on Android if permission request fails', async () => {
|
|
579
|
+
window.minisParams = {...window.minisParams, platform: 'android'} as any
|
|
580
|
+
mockRequestPermission.mockRejectedValue(
|
|
581
|
+
new Error('Permission request failed')
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
const TestComponent = () => {
|
|
585
|
+
const {openCamera} = useImagePickerContext()
|
|
586
|
+
|
|
587
|
+
return (
|
|
588
|
+
<button
|
|
589
|
+
type="button"
|
|
590
|
+
onClick={() =>
|
|
591
|
+
openCamera().catch(error => {
|
|
592
|
+
const span = document.createElement('span')
|
|
593
|
+
span.textContent = error.message
|
|
594
|
+
span.setAttribute('data-testid', 'permission-error')
|
|
595
|
+
document.body.appendChild(span)
|
|
596
|
+
})
|
|
597
|
+
}
|
|
598
|
+
>
|
|
599
|
+
Open Camera
|
|
600
|
+
</button>
|
|
601
|
+
)
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const {container} = render(
|
|
605
|
+
<ImagePickerProvider>
|
|
606
|
+
<TestComponent />
|
|
607
|
+
</ImagePickerProvider>
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
const cameraInput = container.querySelector(
|
|
611
|
+
'input[capture="environment"]'
|
|
612
|
+
) as HTMLInputElement
|
|
613
|
+
const clickSpy = vi.spyOn(cameraInput, 'click')
|
|
614
|
+
|
|
615
|
+
const button = screen.getByText('Open Camera')
|
|
616
|
+
fireEvent.click(button)
|
|
617
|
+
|
|
618
|
+
// Wait for permission request to complete
|
|
619
|
+
await vi.waitFor(() => {
|
|
620
|
+
expect(mockRequestPermission).toHaveBeenCalledWith({
|
|
621
|
+
permission: 'CAMERA',
|
|
622
|
+
})
|
|
623
|
+
})
|
|
624
|
+
|
|
625
|
+
// Input should NOT be clicked when permission request fails
|
|
626
|
+
expect(clickSpy).not.toHaveBeenCalled()
|
|
627
|
+
|
|
628
|
+
// Should show error message
|
|
629
|
+
await vi.waitFor(() => {
|
|
630
|
+
const errorMessage = document.querySelector(
|
|
631
|
+
'[data-testid="permission-error"]'
|
|
632
|
+
)
|
|
633
|
+
expect(errorMessage?.textContent).toBe('Camera permission not granted')
|
|
634
|
+
})
|
|
635
|
+
})
|
|
636
|
+
|
|
637
|
+
it('requests permission for front camera on Android', async () => {
|
|
638
|
+
window.minisParams = {...window.minisParams, platform: 'android'} as any
|
|
639
|
+
mockRequestPermission.mockResolvedValue({granted: true})
|
|
640
|
+
|
|
641
|
+
const TestComponent = () => {
|
|
642
|
+
const {openCamera} = useImagePickerContext()
|
|
643
|
+
|
|
644
|
+
return (
|
|
645
|
+
<button
|
|
646
|
+
type="button"
|
|
647
|
+
onClick={() =>
|
|
648
|
+
openCamera('front').catch(() => {
|
|
649
|
+
// Ignore errors from cleanup
|
|
650
|
+
})
|
|
651
|
+
}
|
|
652
|
+
>
|
|
653
|
+
Open Front Camera
|
|
654
|
+
</button>
|
|
655
|
+
)
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const {container} = render(
|
|
659
|
+
<ImagePickerProvider>
|
|
660
|
+
<TestComponent />
|
|
661
|
+
</ImagePickerProvider>
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
const frontCameraInput = container.querySelector(
|
|
665
|
+
'input[capture="user"]'
|
|
666
|
+
) as HTMLInputElement
|
|
667
|
+
const clickSpy = vi.spyOn(frontCameraInput, 'click')
|
|
668
|
+
|
|
669
|
+
const button = screen.getByText('Open Front Camera')
|
|
670
|
+
fireEvent.click(button)
|
|
671
|
+
|
|
672
|
+
// Wait for permission request to complete
|
|
673
|
+
await vi.waitFor(() => {
|
|
674
|
+
expect(mockRequestPermission).toHaveBeenCalledWith({
|
|
675
|
+
permission: 'CAMERA',
|
|
676
|
+
})
|
|
677
|
+
})
|
|
678
|
+
|
|
679
|
+
// Front camera input should be clicked after permission is granted
|
|
680
|
+
await vi.waitFor(() => {
|
|
681
|
+
expect(clickSpy).toHaveBeenCalledTimes(1)
|
|
682
|
+
})
|
|
683
|
+
})
|
|
359
684
|
})
|
|
360
685
|
|
|
361
686
|
describe('Multiple Picker Handling', () => {
|
|
@@ -7,6 +7,8 @@ import React, {
|
|
|
7
7
|
useMemo,
|
|
8
8
|
} from 'react'
|
|
9
9
|
|
|
10
|
+
import {useRequestPermissions} from '../hooks/util/useRequestPermissions'
|
|
11
|
+
|
|
10
12
|
export type CameraFacing = 'front' | 'back'
|
|
11
13
|
|
|
12
14
|
interface ImagePickerContextValue {
|
|
@@ -30,6 +32,8 @@ interface ImagePickerProviderProps {
|
|
|
30
32
|
children: React.ReactNode
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
const isAndroid = () => window?.minisParams?.platform === 'android'
|
|
36
|
+
|
|
33
37
|
export function ImagePickerProvider({children}: ImagePickerProviderProps) {
|
|
34
38
|
const galleryInputRef = useRef<HTMLInputElement>(null)
|
|
35
39
|
const frontCameraInputRef = useRef<HTMLInputElement>(null)
|
|
@@ -41,6 +45,8 @@ export function ImagePickerProvider({children}: ImagePickerProviderProps) {
|
|
|
41
45
|
handler: () => void
|
|
42
46
|
} | null>(null)
|
|
43
47
|
|
|
48
|
+
const {requestPermission} = useRequestPermissions()
|
|
49
|
+
|
|
44
50
|
const cleanupCancelHandler = useCallback(() => {
|
|
45
51
|
if (activeCancelHandlerRef.current) {
|
|
46
52
|
const {input, handler} = activeCancelHandlerRef.current
|
|
@@ -106,9 +112,22 @@ export function ImagePickerProvider({children}: ImagePickerProviderProps) {
|
|
|
106
112
|
input.addEventListener('cancel', handleCancel)
|
|
107
113
|
activeCancelHandlerRef.current = {input, handler: handleCancel}
|
|
108
114
|
|
|
109
|
-
|
|
115
|
+
if (isAndroid()) {
|
|
116
|
+
// Android requires explicit camera permission for camera picker
|
|
117
|
+
requestPermission({permission: 'CAMERA'})
|
|
118
|
+
.then(() => {
|
|
119
|
+
// This will show both Camera and Gallery
|
|
120
|
+
input.click()
|
|
121
|
+
})
|
|
122
|
+
.catch(() => {
|
|
123
|
+
// Show only Gallery
|
|
124
|
+
input.click()
|
|
125
|
+
})
|
|
126
|
+
} else {
|
|
127
|
+
input.click()
|
|
128
|
+
}
|
|
110
129
|
})
|
|
111
|
-
}, [rejectPendingPromise, cleanupCancelHandler])
|
|
130
|
+
}, [rejectPendingPromise, cleanupCancelHandler, requestPermission])
|
|
112
131
|
|
|
113
132
|
const openCamera = useCallback(
|
|
114
133
|
(cameraFacing: CameraFacing = 'back') => {
|
|
@@ -143,10 +162,29 @@ export function ImagePickerProvider({children}: ImagePickerProviderProps) {
|
|
|
143
162
|
input.addEventListener('cancel', handleCancel)
|
|
144
163
|
activeCancelHandlerRef.current = {input, handler: handleCancel}
|
|
145
164
|
|
|
146
|
-
|
|
165
|
+
if (isAndroid()) {
|
|
166
|
+
// Android requires explicit camera permission
|
|
167
|
+
requestPermission({permission: 'CAMERA'})
|
|
168
|
+
.then(({granted}) => {
|
|
169
|
+
if (granted) {
|
|
170
|
+
input.click()
|
|
171
|
+
} else {
|
|
172
|
+
reject(new Error('Camera permission not granted'))
|
|
173
|
+
resolveRef.current = null
|
|
174
|
+
rejectRef.current = null
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
.catch(() => {
|
|
178
|
+
reject(new Error('Camera permission not granted'))
|
|
179
|
+
resolveRef.current = null
|
|
180
|
+
rejectRef.current = null
|
|
181
|
+
})
|
|
182
|
+
} else {
|
|
183
|
+
input.click()
|
|
184
|
+
}
|
|
147
185
|
})
|
|
148
186
|
},
|
|
149
|
-
[rejectPendingPromise, cleanupCancelHandler]
|
|
187
|
+
[rejectPendingPromise, cleanupCancelHandler, requestPermission]
|
|
150
188
|
)
|
|
151
189
|
|
|
152
190
|
useEffect(() => {
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
const n = {
|
|
2
|
-
MiniConsent: "mini_consent",
|
|
3
|
-
CameraConsent: "camera_consent",
|
|
4
|
-
PhotoLibraryConsent: "photo_library_consent",
|
|
5
|
-
MicrophoneConsent: "microphone_consent"
|
|
6
|
-
}, e = {
|
|
7
|
-
Granted: "granted",
|
|
8
|
-
Dismissed: "dismissed",
|
|
9
|
-
Idle: "idle"
|
|
10
|
-
};
|
|
11
|
-
export {
|
|
12
|
-
n as Consent,
|
|
13
|
-
e as ConsentStatus
|
|
14
|
-
};
|
|
15
|
-
//# sourceMappingURL=permissions.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"permissions.js","sources":["../../../../../shop-minis-platform/src/types/permissions.ts"],"sourcesContent":["export type Permission = 'CAMERA' | 'GALLERY' | 'MICROPHONE'\n\nexport interface MiniManifest {\n trusted_domains?: string[]\n permissions?: string[]\n [key: string]: any\n}\n\nexport const Consent = {\n MiniConsent: 'mini_consent',\n CameraConsent: 'camera_consent',\n PhotoLibraryConsent: 'photo_library_consent',\n MicrophoneConsent: 'microphone_consent',\n} as const\n\nexport const ConsentStatus = {\n Granted: 'granted',\n Dismissed: 'dismissed',\n Idle: 'idle',\n} as const\n\nexport type ConsentType = (typeof Consent)[keyof typeof Consent]\nexport type ConsentStatusType =\n (typeof ConsentStatus)[keyof typeof ConsentStatus]\n"],"names":["Consent","ConsentStatus"],"mappings":"AAQO,MAAMA,IAAU;AAAA,EACrB,aAAa;AAAA,EACb,eAAe;AAAA,EACf,qBAAqB;AAAA,EACrB,mBAAmB;AACrB,GAEaC,IAAgB;AAAA,EAC3B,SAAS;AAAA,EACT,WAAW;AAAA,EACX,MAAM;AACR;"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../../../../../../../../node_modules/.pnpm/react-intersection-observer@9.16.0_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/react-intersection-observer/dist/index.mjs"],"sourcesContent":["\"use client\";\nvar __defProp = Object.defineProperty;\nvar __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;\nvar __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== \"symbol\" ? key + \"\" : key, value);\n\n// src/InView.tsx\nimport * as React from \"react\";\n\n// src/observe.ts\nvar observerMap = /* @__PURE__ */ new Map();\nvar RootIds = /* @__PURE__ */ new WeakMap();\nvar rootId = 0;\nvar unsupportedValue = void 0;\nfunction defaultFallbackInView(inView) {\n unsupportedValue = inView;\n}\nfunction getRootId(root) {\n if (!root) return \"0\";\n if (RootIds.has(root)) return RootIds.get(root);\n rootId += 1;\n RootIds.set(root, rootId.toString());\n return RootIds.get(root);\n}\nfunction optionsToId(options) {\n return Object.keys(options).sort().filter(\n (key) => options[key] !== void 0\n ).map((key) => {\n return `${key}_${key === \"root\" ? getRootId(options.root) : options[key]}`;\n }).toString();\n}\nfunction createObserver(options) {\n const id = optionsToId(options);\n let instance = observerMap.get(id);\n if (!instance) {\n const elements = /* @__PURE__ */ new Map();\n let thresholds;\n const observer = new IntersectionObserver((entries) => {\n entries.forEach((entry) => {\n var _a;\n const inView = entry.isIntersecting && thresholds.some((threshold) => entry.intersectionRatio >= threshold);\n if (options.trackVisibility && typeof entry.isVisible === \"undefined\") {\n entry.isVisible = inView;\n }\n (_a = elements.get(entry.target)) == null ? void 0 : _a.forEach((callback) => {\n callback(inView, entry);\n });\n });\n }, options);\n thresholds = observer.thresholds || (Array.isArray(options.threshold) ? options.threshold : [options.threshold || 0]);\n instance = {\n id,\n observer,\n elements\n };\n observerMap.set(id, instance);\n }\n return instance;\n}\nfunction observe(element, callback, options = {}, fallbackInView = unsupportedValue) {\n if (typeof window.IntersectionObserver === \"undefined\" && fallbackInView !== void 0) {\n const bounds = element.getBoundingClientRect();\n callback(fallbackInView, {\n isIntersecting: fallbackInView,\n target: element,\n intersectionRatio: typeof options.threshold === \"number\" ? options.threshold : 0,\n time: 0,\n boundingClientRect: bounds,\n intersectionRect: bounds,\n rootBounds: bounds\n });\n return () => {\n };\n }\n const { id, observer, elements } = createObserver(options);\n const callbacks = elements.get(element) || [];\n if (!elements.has(element)) {\n elements.set(element, callbacks);\n }\n callbacks.push(callback);\n observer.observe(element);\n return function unobserve() {\n callbacks.splice(callbacks.indexOf(callback), 1);\n if (callbacks.length === 0) {\n elements.delete(element);\n observer.unobserve(element);\n }\n if (elements.size === 0) {\n observer.disconnect();\n observerMap.delete(id);\n }\n };\n}\n\n// src/InView.tsx\nfunction isPlainChildren(props) {\n return typeof props.children !== \"function\";\n}\nvar InView = class extends React.Component {\n constructor(props) {\n super(props);\n __publicField(this, \"node\", null);\n __publicField(this, \"_unobserveCb\", null);\n __publicField(this, \"handleNode\", (node) => {\n if (this.node) {\n this.unobserve();\n if (!node && !this.props.triggerOnce && !this.props.skip) {\n this.setState({ inView: !!this.props.initialInView, entry: void 0 });\n }\n }\n this.node = node ? node : null;\n this.observeNode();\n });\n __publicField(this, \"handleChange\", (inView, entry) => {\n if (inView && this.props.triggerOnce) {\n this.unobserve();\n }\n if (!isPlainChildren(this.props)) {\n this.setState({ inView, entry });\n }\n if (this.props.onChange) {\n this.props.onChange(inView, entry);\n }\n });\n this.state = {\n inView: !!props.initialInView,\n entry: void 0\n };\n }\n componentDidMount() {\n this.unobserve();\n this.observeNode();\n }\n componentDidUpdate(prevProps) {\n if (prevProps.rootMargin !== this.props.rootMargin || prevProps.root !== this.props.root || prevProps.threshold !== this.props.threshold || prevProps.skip !== this.props.skip || prevProps.trackVisibility !== this.props.trackVisibility || prevProps.delay !== this.props.delay) {\n this.unobserve();\n this.observeNode();\n }\n }\n componentWillUnmount() {\n this.unobserve();\n }\n observeNode() {\n if (!this.node || this.props.skip) return;\n const {\n threshold,\n root,\n rootMargin,\n trackVisibility,\n delay,\n fallbackInView\n } = this.props;\n this._unobserveCb = observe(\n this.node,\n this.handleChange,\n {\n threshold,\n root,\n rootMargin,\n // @ts-ignore\n trackVisibility,\n // @ts-ignore\n delay\n },\n fallbackInView\n );\n }\n unobserve() {\n if (this._unobserveCb) {\n this._unobserveCb();\n this._unobserveCb = null;\n }\n }\n render() {\n const { children } = this.props;\n if (typeof children === \"function\") {\n const { inView, entry } = this.state;\n return children({ inView, entry, ref: this.handleNode });\n }\n const {\n as,\n triggerOnce,\n threshold,\n root,\n rootMargin,\n onChange,\n skip,\n trackVisibility,\n delay,\n initialInView,\n fallbackInView,\n ...props\n } = this.props;\n return React.createElement(\n as || \"div\",\n { ref: this.handleNode, ...props },\n children\n );\n }\n};\n\n// src/useInView.tsx\nimport * as React2 from \"react\";\nfunction useInView({\n threshold,\n delay,\n trackVisibility,\n rootMargin,\n root,\n triggerOnce,\n skip,\n initialInView,\n fallbackInView,\n onChange\n} = {}) {\n var _a;\n const [ref, setRef] = React2.useState(null);\n const callback = React2.useRef(onChange);\n const [state, setState] = React2.useState({\n inView: !!initialInView,\n entry: void 0\n });\n callback.current = onChange;\n React2.useEffect(\n () => {\n if (skip || !ref) return;\n let unobserve;\n unobserve = observe(\n ref,\n (inView, entry) => {\n setState({\n inView,\n entry\n });\n if (callback.current) callback.current(inView, entry);\n if (entry.isIntersecting && triggerOnce && unobserve) {\n unobserve();\n unobserve = void 0;\n }\n },\n {\n root,\n rootMargin,\n threshold,\n // @ts-ignore\n trackVisibility,\n // @ts-ignore\n delay\n },\n fallbackInView\n );\n return () => {\n if (unobserve) {\n unobserve();\n }\n };\n },\n // We break the rule here, because we aren't including the actual `threshold` variable\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [\n // If the threshold is an array, convert it to a string, so it won't change between renders.\n Array.isArray(threshold) ? threshold.toString() : threshold,\n ref,\n root,\n rootMargin,\n triggerOnce,\n skip,\n trackVisibility,\n fallbackInView,\n delay\n ]\n );\n const entryTarget = (_a = state.entry) == null ? void 0 : _a.target;\n const previousEntryTarget = React2.useRef(void 0);\n if (!ref && entryTarget && !triggerOnce && !skip && previousEntryTarget.current !== entryTarget) {\n previousEntryTarget.current = entryTarget;\n setState({\n inView: !!initialInView,\n entry: void 0\n });\n }\n const result = [setRef, state.inView, state.entry];\n result.ref = result[0];\n result.inView = result[1];\n result.entry = result[2];\n return result;\n}\nexport {\n InView,\n defaultFallbackInView,\n observe,\n useInView\n};\n//# sourceMappingURL=index.mjs.map"],"names":["__defProp","__defNormalProp","obj","key","value","__publicField","observerMap","RootIds","rootId","unsupportedValue","getRootId","root","optionsToId","options","createObserver","id","instance","elements","thresholds","observer","entries","entry","_a","inView","threshold","callback","observe","element","fallbackInView","bounds","callbacks","isPlainChildren","props","InView","React","node","prevProps","rootMargin","trackVisibility","delay","children","as","triggerOnce","onChange","skip","initialInView"],"mappings":"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","x_google_ignoreList":[0]}
|