cozy-sharing 28.7.0 → 28.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,18 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [28.9.0](https://github.com/cozy/cozy-libs/compare/cozy-sharing@28.8.0...cozy-sharing@28.9.0) (2026-03-12)
7
+
8
+ ### Features
9
+
10
+ - Disable Twake Creation from public sharing link ([e8e6fcf](https://github.com/cozy/cozy-libs/commit/e8e6fcfc5e659796f06378818044debb09c7c4e2))
11
+
12
+ # [28.8.0](https://github.com/cozy/cozy-libs/compare/cozy-sharing@28.7.0...cozy-sharing@28.8.0) (2026-03-12)
13
+
14
+ ### Features
15
+
16
+ - **cozy-sharing:** Use FederatedFolderModal on existing sharings ([541de13](https://github.com/cozy/cozy-libs/commit/541de1310631170b5567694ff1366bc997fb3ea3))
17
+
6
18
  # [28.7.0](https://github.com/cozy/cozy-libs/compare/cozy-sharing@28.6.0...cozy-sharing@28.7.0) (2026-03-12)
7
19
 
8
20
  ### Features
@@ -1,3 +1,4 @@
1
+ import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
1
2
  import _asyncToGenerator from "@babel/runtime/helpers/asyncToGenerator";
2
3
  import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
3
4
  import _regeneratorRuntime from "@babel/runtime/regenerator";
@@ -24,7 +25,9 @@ export var FederatedFolderModal = withLocales(function (_ref) {
24
25
  getFederatedShareLink = _useSharingContext.getFederatedShareLink,
25
26
  getDocumentPermissions = _useSharingContext.getDocumentPermissions,
26
27
  getOwner = _useSharingContext.getOwner,
27
- getSharingById = _useSharingContext.getSharingById;
28
+ getSharingById = _useSharingContext.getSharingById,
29
+ getRecipients = _useSharingContext.getRecipients,
30
+ revoke = _useSharingContext.revoke;
28
31
 
29
32
  var _useAlert = useAlert(),
30
33
  showAlert = _useAlert.showAlert;
@@ -162,21 +165,50 @@ export var FederatedFolderModal = withLocales(function (_ref) {
162
165
  }
163
166
  };
164
167
 
165
- var onRevoke = function onRevoke(index) {
166
- var _id = index.split(RECIPIENT_INDEX_PREFIX)[1];
167
- setFederatedRecipients(function (prev) {
168
- return {
169
- recipients: prev.recipients.filter(function (r) {
170
- return r._id !== _id;
171
- }),
172
- readOnlyRecipients: prev.readOnlyRecipients.filter(function (r) {
173
- return r._id !== _id;
174
- })
175
- };
176
- });
177
- };
168
+ var onRevoke = /*#__PURE__*/function () {
169
+ var _ref4 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee3(documentOrIndex, sharingId, memberIndex) {
170
+ var _id;
171
+
172
+ return _regeneratorRuntime.wrap(function _callee3$(_context3) {
173
+ while (1) switch (_context3.prev = _context3.next) {
174
+ case 0:
175
+ if (!(typeof documentOrIndex === 'string' && documentOrIndex.startsWith(RECIPIENT_INDEX_PREFIX))) {
176
+ _context3.next = 5;
177
+ break;
178
+ }
179
+
180
+ _id = documentOrIndex.split(RECIPIENT_INDEX_PREFIX)[1];
181
+ setFederatedRecipients(function (prev) {
182
+ return {
183
+ recipients: prev.recipients.filter(function (r) {
184
+ return r._id !== _id;
185
+ }),
186
+ readOnlyRecipients: prev.readOnlyRecipients.filter(function (r) {
187
+ return r._id !== _id;
188
+ })
189
+ };
190
+ });
191
+ _context3.next = 7;
192
+ break;
193
+
194
+ case 5:
195
+ _context3.next = 7;
196
+ return revoke(documentOrIndex, sharingId, memberIndex);
197
+
198
+ case 7:
199
+ case "end":
200
+ return _context3.stop();
201
+ }
202
+ }, _callee3);
203
+ }));
204
+
205
+ return function onRevoke(_x, _x2, _x3) {
206
+ return _ref4.apply(this, arguments);
207
+ };
208
+ }();
178
209
 
179
- var recipients = formatRecipients(federatedRecipients);
210
+ var existingRecipients = existingDocument ? getRecipients(existingDocument._id) : [];
211
+ var recipients = [].concat(_toConsumableArray(existingRecipients), _toConsumableArray(formatRecipients(federatedRecipients)));
180
212
  var modalTitle = t('FederatedFolder.shareTitle', {
181
213
  name: folderName
182
214
  });
@@ -189,7 +221,7 @@ export var FederatedFolderModal = withLocales(function (_ref) {
189
221
  },
190
222
  recipients: recipients,
191
223
  readOnlyRecipients: federatedRecipients.readOnlyRecipients,
192
- currentRecipients: [],
224
+ currentRecipients: existingRecipients,
193
225
  onRevoke: onRevoke,
194
226
  onSetType: onSetType,
195
227
  onSend: onSend,
@@ -7,7 +7,9 @@ import { createMockClient } from 'cozy-client';
7
7
  import { FederatedFolderModal } from './FederatedFolderModal';
8
8
  import AppLike from '../SharingBanner/test/AppLike';
9
9
  var mockShare = jest.fn();
10
+ var mockRevoke = jest.fn();
10
11
  var mockGetSharingLink = jest.fn();
12
+ var mockGetRecipients = jest.fn().mockReturnValue([]);
11
13
  var mockShowAlert = jest.fn();
12
14
  var mockOnClose = jest.fn();
13
15
  jest.mock('../../hooks/useSharingContext', function () {
@@ -15,8 +17,10 @@ jest.mock('../../hooks/useSharingContext', function () {
15
17
  useSharingContext: function useSharingContext() {
16
18
  return {
17
19
  share: mockShare,
20
+ revoke: mockRevoke,
18
21
  getSharingLink: mockGetSharingLink,
19
- getDocumentPermissions: jest.fn().mockReturnValue([])
22
+ getDocumentPermissions: jest.fn().mockReturnValue([]),
23
+ getRecipients: mockGetRecipients
20
24
  };
21
25
  }
22
26
  };
@@ -91,6 +95,13 @@ jest.mock('./DumbFederatedFolderModal', function () {
91
95
  return onRevoke('virtual-shared-drive-sharing-r1');
92
96
  }
93
97
  }, "Revoke"), /*#__PURE__*/React.createElement("button", {
98
+ "data-testid": "btn-revoke-existing",
99
+ onClick: function onClick() {
100
+ return onRevoke({
101
+ _id: 'folder-123'
102
+ }, 'abc', 1);
103
+ }
104
+ }, "Revoke Existing"), /*#__PURE__*/React.createElement("button", {
94
105
  "data-testid": "btn-close",
95
106
  onClick: onClose
96
107
  }, "Close"), /*#__PURE__*/React.createElement("button", {
@@ -150,6 +161,7 @@ describe('FederatedFolderModal', function () {
150
161
  beforeEach(function () {
151
162
  jest.clearAllMocks();
152
163
  mockGetSharingLink.mockReturnValue('https://example.com/share/abc123');
164
+ mockGetRecipients.mockReturnValue([]);
153
165
  client = createTestClient();
154
166
  sharingContextValue = createSharingContextValue();
155
167
  });
@@ -511,22 +523,109 @@ describe('FederatedFolderModal', function () {
511
523
  }, _callee12);
512
524
  })));
513
525
  });
514
- describe('createContact callback', function () {
515
- it('should call client.create when creating a contact', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee13() {
526
+ describe('existing recipients', function () {
527
+ var existingRecipients = [{
528
+ index: 'sharing-abc-member-0',
529
+ name: 'Charlie',
530
+ email: 'charlie@example.com',
531
+ status: 'owner',
532
+ type: 'two-way',
533
+ sharingId: 'abc',
534
+ memberIndex: 0
535
+ }, {
536
+ index: 'sharing-abc-member-1',
537
+ name: 'Diana',
538
+ email: 'diana@example.com',
539
+ status: 'ready',
540
+ type: 'one-way',
541
+ sharingId: 'abc',
542
+ memberIndex: 1
543
+ }];
544
+ it('should display existing recipients from getRecipients', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee13() {
516
545
  var _setup13, getByTestId;
517
546
 
518
547
  return _regeneratorRuntime.wrap(function _callee13$(_context13) {
519
548
  while (1) switch (_context13.prev = _context13.next) {
520
549
  case 0:
550
+ mockGetRecipients.mockReturnValue(existingRecipients);
521
551
  _setup13 = setup(), getByTestId = _setup13.getByTestId;
522
- _context13.next = 3;
552
+ _context13.next = 4;
553
+ return waitFor(function () {
554
+ expect(getByTestId('recipients-count').textContent).toBe('2');
555
+ });
556
+
557
+ case 4:
558
+ case "end":
559
+ return _context13.stop();
560
+ }
561
+ }, _callee13);
562
+ })));
563
+ it('should pass existing recipients as currentRecipients', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee14() {
564
+ var _setup14, getByTestId;
565
+
566
+ return _regeneratorRuntime.wrap(function _callee14$(_context14) {
567
+ while (1) switch (_context14.prev = _context14.next) {
568
+ case 0:
569
+ mockGetRecipients.mockReturnValue(existingRecipients);
570
+ _setup14 = setup(), getByTestId = _setup14.getByTestId;
571
+ _context14.next = 4;
572
+ return waitFor(function () {
573
+ expect(getByTestId('recipients-count').textContent).toBe('2');
574
+ });
575
+
576
+ case 4:
577
+ case "end":
578
+ return _context14.stop();
579
+ }
580
+ }, _callee14);
581
+ })));
582
+ it('should call revoke for existing member', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee15() {
583
+ var _setup15, getByTestId;
584
+
585
+ return _regeneratorRuntime.wrap(function _callee15$(_context15) {
586
+ while (1) switch (_context15.prev = _context15.next) {
587
+ case 0:
588
+ mockGetRecipients.mockReturnValue(existingRecipients);
589
+ mockRevoke.mockResolvedValueOnce({});
590
+ _setup15 = setup(), getByTestId = _setup15.getByTestId;
591
+ _context15.next = 5;
592
+ return waitFor(function () {
593
+ expect(getByTestId('dumb-modal')).toBeTruthy();
594
+ });
595
+
596
+ case 5:
597
+ // Simulate revoke of an existing member (non-virtual-shared-drive index)
598
+ fireEvent.click(getByTestId('btn-revoke-existing'));
599
+ _context15.next = 8;
600
+ return waitFor(function () {
601
+ expect(mockRevoke).toHaveBeenCalledWith({
602
+ _id: 'folder-123'
603
+ }, 'abc', 1);
604
+ });
605
+
606
+ case 8:
607
+ case "end":
608
+ return _context15.stop();
609
+ }
610
+ }, _callee15);
611
+ })));
612
+ });
613
+ describe('createContact callback', function () {
614
+ it('should call client.create when creating a contact', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee16() {
615
+ var _setup16, getByTestId;
616
+
617
+ return _regeneratorRuntime.wrap(function _callee16$(_context16) {
618
+ while (1) switch (_context16.prev = _context16.next) {
619
+ case 0:
620
+ _setup16 = setup(), getByTestId = _setup16.getByTestId;
621
+ _context16.next = 3;
523
622
  return waitFor(function () {
524
623
  expect(getByTestId('btn-create-contact')).toBeTruthy();
525
624
  });
526
625
 
527
626
  case 3:
528
627
  fireEvent.click(getByTestId('btn-create-contact'));
529
- _context13.next = 6;
628
+ _context16.next = 6;
530
629
  return waitFor(function () {
531
630
  expect(client.create).toHaveBeenCalledWith('io.cozy.contacts', {
532
631
  email: 'test@example.com'
@@ -535,9 +634,9 @@ describe('FederatedFolderModal', function () {
535
634
 
536
635
  case 6:
537
636
  case "end":
538
- return _context13.stop();
637
+ return _context16.stop();
539
638
  }
540
- }, _callee13);
639
+ }, _callee16);
541
640
  })));
542
641
  });
543
642
  });
@@ -14,7 +14,7 @@ import withLocales from '../../hoc/withLocales';
14
14
  import { useSharingContext } from '../../hooks/useSharingContext';
15
15
  import { FederatedFolderModal } from '../FederatedFolder/FederatedFolderModal';
16
16
  export var ShareModal = withLocales(function (props) {
17
- var _location$state, _byDocId$document$id;
17
+ var _location$state;
18
18
 
19
19
  var location = useLocation === null || useLocation === void 0 ? void 0 : useLocation();
20
20
  var locationProps = location === null || location === void 0 || (_location$state = location.state) === null || _location$state === void 0 ? void 0 : _location$state.modalProps;
@@ -59,12 +59,8 @@ export var ShareModal = withLocales(function (props) {
59
59
  }();
60
60
 
61
61
  var isFederatedMode = flag('drive.federated-shared-folder.enabled');
62
- var hasSharings = ((_byDocId$document$id = byDocId[document.id]) === null || _byDocId$document$id === void 0 || (_byDocId$document$id = _byDocId$document$id.sharings) === null || _byDocId$document$id === void 0 ? void 0 : _byDocId$document$id.length) > 0; // In federated mode, show FederatedFolderModal for unshared documents (sharing an existing folder).
63
- // When isFederatedMode && hasSharings is true, we intentionally fall through to the legacy
64
- // modals (EditableSharingModal / SharingDetailsModal) because federated-shared folders are
65
- // handled by those components.
66
62
 
67
- if (isFederatedMode && allLoaded && !hasSharings) {
63
+ if (isFederatedMode && allLoaded) {
68
64
  return /*#__PURE__*/React.createElement(FederatedFolderModal, {
69
65
  document: document,
70
66
  onClose: rest.onClose
@@ -123,6 +123,12 @@ var SharingBannerByLink = function SharingBannerByLink(_ref3) {
123
123
  t = _useI18n4.t;
124
124
 
125
125
  var HOME_LINK_HREF = flag('signup.url');
126
+ var canNotCreateTwakeFromPublicSharingLink = flag('drive.twake-creation-from-public-sharing.disabled');
127
+
128
+ if (canNotCreateTwakeFromPublicSharingLink) {
129
+ return null;
130
+ }
131
+
126
132
  return /*#__PURE__*/React.createElement(Banner, {
127
133
  bgcolor: "var(--defaultBackgroundColor)",
128
134
  text: /*#__PURE__*/React.createElement(SharingBannerByLinkText, null),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cozy-sharing",
3
- "version": "28.7.0",
3
+ "version": "28.9.0",
4
4
  "description": "Provides sharing login for React applications.",
5
5
  "main": "dist/index.js",
6
6
  "author": "Cozy",
@@ -83,5 +83,5 @@
83
83
  "sideEffects": [
84
84
  "*.css"
85
85
  ],
86
- "gitHead": "421077ecc24c9a15a3a1c22d9c3442917292f8ba"
86
+ "gitHead": "53934e510ce08e3066a3b6aad6d9bb05778dede8"
87
87
  }
@@ -25,7 +25,9 @@ export const FederatedFolderModal = withLocales(
25
25
  getFederatedShareLink,
26
26
  getDocumentPermissions,
27
27
  getOwner,
28
- getSharingById
28
+ getSharingById,
29
+ getRecipients,
30
+ revoke
29
31
  } = useSharingContext()
30
32
  const { showAlert } = useAlert()
31
33
 
@@ -107,18 +109,29 @@ export const FederatedFolderModal = withLocales(
107
109
  }
108
110
  }
109
111
 
110
- const onRevoke = index => {
111
- const _id = index.split(RECIPIENT_INDEX_PREFIX)[1]
112
-
113
- setFederatedRecipients(prev => {
114
- return {
112
+ const onRevoke = async (documentOrIndex, sharingId, memberIndex) => {
113
+ if (
114
+ typeof documentOrIndex === 'string' &&
115
+ documentOrIndex.startsWith(RECIPIENT_INDEX_PREFIX)
116
+ ) {
117
+ const _id = documentOrIndex.split(RECIPIENT_INDEX_PREFIX)[1]
118
+ setFederatedRecipients(prev => ({
115
119
  recipients: prev.recipients.filter(r => r._id !== _id),
116
120
  readOnlyRecipients: prev.readOnlyRecipients.filter(r => r._id !== _id)
117
- }
118
- })
121
+ }))
122
+ } else {
123
+ await revoke(documentOrIndex, sharingId, memberIndex)
124
+ }
119
125
  }
120
126
 
121
- const recipients = formatRecipients(federatedRecipients)
127
+ const existingRecipients = existingDocument
128
+ ? getRecipients(existingDocument._id)
129
+ : []
130
+
131
+ const recipients = [
132
+ ...existingRecipients,
133
+ ...formatRecipients(federatedRecipients)
134
+ ]
122
135
 
123
136
  const modalTitle = t('FederatedFolder.shareTitle', { name: folderName })
124
137
 
@@ -131,7 +144,7 @@ export const FederatedFolderModal = withLocales(
131
144
  createContact={contact => client.create('io.cozy.contacts', contact)}
132
145
  recipients={recipients}
133
146
  readOnlyRecipients={federatedRecipients.readOnlyRecipients}
134
- currentRecipients={[]}
147
+ currentRecipients={existingRecipients}
135
148
  onRevoke={onRevoke}
136
149
  onSetType={onSetType}
137
150
  onSend={onSend}
@@ -7,15 +7,19 @@ import { FederatedFolderModal } from './FederatedFolderModal'
7
7
  import AppLike from '../SharingBanner/test/AppLike'
8
8
 
9
9
  const mockShare = jest.fn()
10
+ const mockRevoke = jest.fn()
10
11
  const mockGetSharingLink = jest.fn()
12
+ const mockGetRecipients = jest.fn().mockReturnValue([])
11
13
  const mockShowAlert = jest.fn()
12
14
  const mockOnClose = jest.fn()
13
15
 
14
16
  jest.mock('../../hooks/useSharingContext', () => ({
15
17
  useSharingContext: () => ({
16
18
  share: mockShare,
19
+ revoke: mockRevoke,
17
20
  getSharingLink: mockGetSharingLink,
18
- getDocumentPermissions: jest.fn().mockReturnValue([])
21
+ getDocumentPermissions: jest.fn().mockReturnValue([]),
22
+ getRecipients: mockGetRecipients
19
23
  })
20
24
  }))
21
25
 
@@ -79,6 +83,12 @@ jest.mock('./DumbFederatedFolderModal', () => ({
79
83
  >
80
84
  Revoke
81
85
  </button>
86
+ <button
87
+ data-testid="btn-revoke-existing"
88
+ onClick={() => onRevoke({ _id: 'folder-123' }, 'abc', 1)}
89
+ >
90
+ Revoke Existing
91
+ </button>
82
92
  <button data-testid="btn-close" onClick={onClose}>
83
93
  Close
84
94
  </button>
@@ -135,6 +145,7 @@ describe('FederatedFolderModal', () => {
135
145
  beforeEach(() => {
136
146
  jest.clearAllMocks()
137
147
  mockGetSharingLink.mockReturnValue('https://example.com/share/abc123')
148
+ mockGetRecipients.mockReturnValue([])
138
149
  client = createTestClient()
139
150
  sharingContextValue = createSharingContextValue()
140
151
  })
@@ -356,6 +367,64 @@ describe('FederatedFolderModal', () => {
356
367
  })
357
368
  })
358
369
 
370
+ describe('existing recipients', () => {
371
+ const existingRecipients = [
372
+ {
373
+ index: 'sharing-abc-member-0',
374
+ name: 'Charlie',
375
+ email: 'charlie@example.com',
376
+ status: 'owner',
377
+ type: 'two-way',
378
+ sharingId: 'abc',
379
+ memberIndex: 0
380
+ },
381
+ {
382
+ index: 'sharing-abc-member-1',
383
+ name: 'Diana',
384
+ email: 'diana@example.com',
385
+ status: 'ready',
386
+ type: 'one-way',
387
+ sharingId: 'abc',
388
+ memberIndex: 1
389
+ }
390
+ ]
391
+
392
+ it('should display existing recipients from getRecipients', async () => {
393
+ mockGetRecipients.mockReturnValue(existingRecipients)
394
+ const { getByTestId } = setup()
395
+
396
+ await waitFor(() => {
397
+ expect(getByTestId('recipients-count').textContent).toBe('2')
398
+ })
399
+ })
400
+
401
+ it('should pass existing recipients as currentRecipients', async () => {
402
+ mockGetRecipients.mockReturnValue(existingRecipients)
403
+ const { getByTestId } = setup()
404
+
405
+ await waitFor(() => {
406
+ expect(getByTestId('recipients-count').textContent).toBe('2')
407
+ })
408
+ })
409
+
410
+ it('should call revoke for existing member', async () => {
411
+ mockGetRecipients.mockReturnValue(existingRecipients)
412
+ mockRevoke.mockResolvedValueOnce({})
413
+ const { getByTestId } = setup()
414
+
415
+ await waitFor(() => {
416
+ expect(getByTestId('dumb-modal')).toBeTruthy()
417
+ })
418
+
419
+ // Simulate revoke of an existing member (non-virtual-shared-drive index)
420
+ fireEvent.click(getByTestId('btn-revoke-existing'))
421
+
422
+ await waitFor(() => {
423
+ expect(mockRevoke).toHaveBeenCalledWith({ _id: 'folder-123' }, 'abc', 1)
424
+ })
425
+ })
426
+ })
427
+
359
428
  describe('createContact callback', () => {
360
429
  it('should call client.create when creating a contact', async () => {
361
430
  const { getByTestId } = setup()
@@ -35,13 +35,7 @@ export const ShareModal = withLocales(props => {
35
35
  }
36
36
 
37
37
  const isFederatedMode = flag('drive.federated-shared-folder.enabled')
38
- const hasSharings = byDocId[document.id]?.sharings?.length > 0
39
-
40
- // In federated mode, show FederatedFolderModal for unshared documents (sharing an existing folder).
41
- // When isFederatedMode && hasSharings is true, we intentionally fall through to the legacy
42
- // modals (EditableSharingModal / SharingDetailsModal) because federated-shared folders are
43
- // handled by those components.
44
- if (isFederatedMode && allLoaded && !hasSharings) {
38
+ if (isFederatedMode && allLoaded) {
45
39
  return <FederatedFolderModal document={document} onClose={rest.onClose} />
46
40
  }
47
41
 
@@ -113,6 +113,13 @@ const SharingBannerByLink = ({ onClose }) => {
113
113
  const { t } = useI18n()
114
114
 
115
115
  const HOME_LINK_HREF = flag('signup.url')
116
+ const canNotCreateTwakeFromPublicSharingLink = flag(
117
+ 'drive.twake-creation-from-public-sharing.disabled'
118
+ )
119
+
120
+ if (canNotCreateTwakeFromPublicSharingLink) {
121
+ return null
122
+ }
116
123
 
117
124
  return (
118
125
  <Banner