cozy-sharing 30.2.0 → 30.2.2

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,16 @@
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
+ ## [30.2.2](https://github.com/cozy/cozy-libs/compare/cozy-sharing@30.2.1...cozy-sharing@30.2.2) (2026-04-22)
7
+
8
+ **Note:** Version bump only for package cozy-sharing
9
+
10
+ ## [30.2.1](https://github.com/cozy/cozy-libs/compare/cozy-sharing@30.2.0...cozy-sharing@30.2.1) (2026-04-20)
11
+
12
+ ### Bug Fixes
13
+
14
+ - **cozy-sharing:** Preserve user gesture for Safari clipboard writes ([c3e9069](https://github.com/cozy/cozy-libs/commit/c3e9069e250880b9d6640c8ae7529b987af497dc))
15
+
6
16
  # [30.2.0](https://github.com/cozy/cozy-libs/compare/cozy-sharing@30.1.1...cozy-sharing@30.2.0) (2026-04-20)
7
17
 
8
18
  ### Features
@@ -118,48 +118,55 @@ var ShareByLink = function ShareByLink(_ref) {
118
118
 
119
119
  var generateLinkAndCopyLinkToClipboard = /*#__PURE__*/function () {
120
120
  var _ref3 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee2() {
121
- var linkToCopy, wasNewlyGenerated;
121
+ var generatedLink;
122
122
  return _regeneratorRuntime.wrap(function _callee2$(_context2) {
123
123
  while (1) switch (_context2.prev = _context2.next) {
124
124
  case 0:
125
- linkToCopy = link;
126
- wasNewlyGenerated = !linkToCopy;
127
-
128
- if (linkToCopy) {
129
- _context2.next = 6;
125
+ if (!link) {
126
+ _context2.next = 4;
130
127
  break;
131
128
  }
132
129
 
133
- _context2.next = 5;
134
- return handleGenerateLink();
135
-
136
- case 5:
137
- linkToCopy = _context2.sent;
130
+ _context2.next = 3;
131
+ return copyToClipboard(link, {
132
+ t: t,
133
+ showAlert: showAlert
134
+ });
138
135
 
139
- case 6:
140
- if (!linkToCopy) {
141
- _context2.next = 12;
142
- break;
143
- }
136
+ case 3:
137
+ return _context2.abrupt("return");
144
138
 
145
- _context2.next = 9;
146
- return copyToClipboard(linkToCopy, {
139
+ case 4:
140
+ // Safari: the clipboard API must be invoked synchronously within the
141
+ // click handler, so we pass the pending link to `copyToClipboard`.
142
+ // The resolved URL is captured here so we can branch on success after
143
+ // the copy has completed, without re-awaiting the promise.
144
+ generatedLink = null;
145
+ _context2.next = 7;
146
+ return copyToClipboard(handleGenerateLink().then(function (generated) {
147
+ generatedLink = generated;
148
+ if (!generated) throw new Error('link-generation-failed');
149
+ return generated;
150
+ }), {
147
151
  t: t,
148
152
  showAlert: showAlert
149
153
  });
150
154
 
151
- case 9:
152
- if (wasNewlyGenerated && autoOpenShareRestriction) {
153
- toggleEditDialogOpen();
155
+ case 7:
156
+ if (generatedLink) {
157
+ _context2.next = 10;
158
+ break;
154
159
  }
155
160
 
156
- _context2.next = 13;
157
- break;
158
-
159
- case 12:
160
161
  showGenericError();
162
+ return _context2.abrupt("return");
161
163
 
162
- case 13:
164
+ case 10:
165
+ if (autoOpenShareRestriction) {
166
+ toggleEditDialogOpen();
167
+ }
168
+
169
+ case 11:
163
170
  case "end":
164
171
  return _context2.stop();
165
172
  }
@@ -91,96 +91,83 @@ export var ShareRestrictionModal = function ShareRestrictionModal(_ref) {
91
91
 
92
92
  var handleClick = /*#__PURE__*/function () {
93
93
  var _ref2 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee() {
94
- var ttl, _yield$createPermissi2, perms, url, _yield$updatePermissi3, _yield$updatePermissi4, _perms, _url;
95
-
94
+ var permsPromise, urlPromise;
96
95
  return _regeneratorRuntime.wrap(function _callee$(_context) {
97
96
  while (1) switch (_context.prev = _context.next) {
98
97
  case 0:
99
- setLoading(true); // If the file is not shared, we create a new sharing link
100
-
101
- if (hasSharingLink) {
102
- _context.next = 13;
103
- break;
104
- }
98
+ setLoading(true); // The clipboard write must be initiated synchronously within the click
99
+ // handler for Safari to honor the user gesture; we pass a pending blob
100
+ // whose content resolves after the permission round-trip.
105
101
 
106
- ttl = makeTTL(dateToggle && selectedDate);
107
- _context.next = 5;
108
- return createPermissions({
102
+ permsPromise = hasSharingLink ? updatePermissions({
109
103
  file: file,
110
104
  t: t,
111
- ttl: ttl,
105
+ dateToggle: dateToggle,
106
+ selectedDate: selectedDate,
107
+ passwordToggle: passwordToggle,
112
108
  password: password,
113
109
  editingRights: editingRights,
114
110
  documentType: documentType,
115
- shareByLink: shareByLink,
116
- showAlert: showAlert
117
- });
118
-
119
- case 5:
120
- _yield$createPermissi2 = _context.sent;
121
- perms = _yield$createPermissi2.data;
122
- url = generateWebLink({
123
- cozyUrl: client.getStackClient().uri,
124
- searchParams: [['sharecode', perms.attributes.shortcodes.code]],
125
- pathname: '/public',
126
- slug: getAppSlugFromDocumentType({
127
- documentType: documentType
128
- }),
129
- subDomainType: client.capabilities.flat_subdomains ? 'flat' : 'nested'
130
- });
131
- _context.next = 10;
132
- return copyToClipboard(url, {
133
- t: t,
111
+ updateDocumentPermissions: updateDocumentPermissions,
134
112
  showAlert: showAlert
135
- });
136
-
137
- case 10:
138
- onClose();
139
- _context.next = 22;
140
- break;
113
+ }).then(function (_ref3) {
114
+ var _ref4 = _slicedToArray(_ref3, 1),
115
+ perms = _ref4[0].data;
141
116
 
142
- case 13:
143
- _context.next = 15;
144
- return updatePermissions({
117
+ return perms;
118
+ }) : createPermissions({
145
119
  file: file,
146
120
  t: t,
147
- dateToggle: dateToggle,
148
- selectedDate: selectedDate,
149
- passwordToggle: passwordToggle,
121
+ ttl: makeTTL(dateToggle && selectedDate),
150
122
  password: password,
151
123
  editingRights: editingRights,
152
124
  documentType: documentType,
153
- updateDocumentPermissions: updateDocumentPermissions,
125
+ shareByLink: shareByLink,
154
126
  showAlert: showAlert
127
+ }).then(function (_ref5) {
128
+ var perms = _ref5.data;
129
+ return perms;
155
130
  });
156
-
157
- case 15:
158
- _yield$updatePermissi3 = _context.sent;
159
- _yield$updatePermissi4 = _slicedToArray(_yield$updatePermissi3, 1);
160
- _perms = _yield$updatePermissi4[0].data;
161
- _url = generateWebLink({
162
- cozyUrl: client.getStackClient().uri,
163
- searchParams: [['sharecode', _perms.attributes.shortcodes.code]],
164
- pathname: '/public',
165
- slug: getAppSlugFromDocumentType({
166
- documentType: documentType
167
- }),
168
- subDomainType: client.capabilities.flat_subdomains ? 'flat' : 'nested'
131
+ urlPromise = permsPromise.then(function (perms) {
132
+ return generateWebLink({
133
+ cozyUrl: client.getStackClient().uri,
134
+ searchParams: [['sharecode', perms.attributes.shortcodes.code]],
135
+ pathname: '/public',
136
+ slug: getAppSlugFromDocumentType({
137
+ documentType: documentType
138
+ }),
139
+ subDomainType: client.capabilities.flat_subdomains ? 'flat' : 'nested'
140
+ });
169
141
  });
170
- _context.next = 21;
171
- return copyToClipboard(_url, {
142
+ _context.next = 5;
143
+ return copyToClipboard(urlPromise, {
172
144
  t: t,
173
145
  showAlert: showAlert
174
146
  });
175
147
 
176
- case 21:
148
+ case 5:
149
+ _context.prev = 5;
150
+ _context.next = 8;
151
+ return permsPromise;
152
+
153
+ case 8:
154
+ _context.next = 14;
155
+ break;
156
+
157
+ case 10:
158
+ _context.prev = 10;
159
+ _context.t0 = _context["catch"](5);
160
+ setLoading(false);
161
+ return _context.abrupt("return");
162
+
163
+ case 14:
177
164
  onClose();
178
165
 
179
- case 22:
166
+ case 15:
180
167
  case "end":
181
168
  return _context.stop();
182
169
  }
183
- }, _callee);
170
+ }, _callee, null, [[5, 10]]);
184
171
  }));
185
172
 
186
173
  return function handleClick() {
@@ -189,7 +176,7 @@ export var ShareRestrictionModal = function ShareRestrictionModal(_ref) {
189
176
  }();
190
177
 
191
178
  var handleRevokeLink = /*#__PURE__*/function () {
192
- var _ref3 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee2() {
179
+ var _ref6 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee2() {
193
180
  return _regeneratorRuntime.wrap(function _callee2$(_context2) {
194
181
  while (1) switch (_context2.prev = _context2.next) {
195
182
  case 0:
@@ -214,7 +201,7 @@ export var ShareRestrictionModal = function ShareRestrictionModal(_ref) {
214
201
  }));
215
202
 
216
203
  return function handleRevokeLink() {
217
- return _ref3.apply(this, arguments);
204
+ return _ref6.apply(this, arguments);
218
205
  };
219
206
  }();
220
207
 
@@ -115,11 +115,19 @@ export var generateShareLinkFromFile = function generateShareLinkFromFile(_ref3)
115
115
  });
116
116
  };
117
117
  /**
118
- * Copy a string to the clipboard
118
+ * Copy a value to the clipboard.
119
119
  *
120
- * @param {string} value - The string saved to the clipboard
120
+ * Accepts a resolved string or a `Promise<string>`. The promise form is
121
+ * required when the value depends on a prior async step (e.g. creating a
122
+ * sharing link) from within a click handler: Safari drops the user-gesture
123
+ * context across any `await`, so `navigator.clipboard.writeText` called after
124
+ * an awaited call throws `NotAllowedError`. Passing a pending blob to
125
+ * `ClipboardItem` lets the clipboard API be invoked synchronously while the
126
+ * content is resolved later.
127
+ *
128
+ * @param {string | Promise<string>} value - Value to copy
121
129
  * @param {object} [options]
122
- * @param {number} options.t - Translation function
130
+ * @param {Function} options.t - Translation function
123
131
  * @param {Function} options.showAlert - Function to display an alert
124
132
  */
125
133
 
@@ -128,6 +136,7 @@ export var copyToClipboard = /*#__PURE__*/function () {
128
136
  var _ref5,
129
137
  t,
130
138
  showAlert,
139
+ blobPromise,
131
140
  _args2 = arguments;
132
141
 
133
142
  return _regeneratorRuntime.wrap(function _callee2$(_context2) {
@@ -144,33 +153,73 @@ export var copyToClipboard = /*#__PURE__*/function () {
144
153
 
145
154
  case 3:
146
155
  _context2.prev = 3;
147
- _context2.next = 6;
156
+
157
+ if (!(typeof value === 'string')) {
158
+ _context2.next = 9;
159
+ break;
160
+ }
161
+
162
+ _context2.next = 7;
148
163
  return navigator.clipboard.writeText(value);
149
164
 
150
- case 6:
165
+ case 7:
166
+ _context2.next = 21;
167
+ break;
168
+
169
+ case 9:
170
+ if (!(typeof ClipboardItem !== 'undefined')) {
171
+ _context2.next = 15;
172
+ break;
173
+ }
174
+
175
+ blobPromise = Promise.resolve(value).then(function (text) {
176
+ return new Blob([text], {
177
+ type: 'text/plain'
178
+ });
179
+ });
180
+ _context2.next = 13;
181
+ return navigator.clipboard.write([new ClipboardItem({
182
+ 'text/plain': blobPromise
183
+ })]);
184
+
185
+ case 13:
186
+ _context2.next = 21;
187
+ break;
188
+
189
+ case 15:
190
+ _context2.t0 = navigator.clipboard;
191
+ _context2.next = 18;
192
+ return value;
193
+
194
+ case 18:
195
+ _context2.t1 = _context2.sent;
196
+ _context2.next = 21;
197
+ return _context2.t0.writeText.call(_context2.t0, _context2.t1);
198
+
199
+ case 21:
151
200
  showAlert({
152
201
  message: t('copyReminderContent.success'),
153
202
  severity: 'success',
154
203
  variant: 'filled'
155
204
  });
156
- _context2.next = 13;
205
+ _context2.next = 28;
157
206
  break;
158
207
 
159
- case 9:
160
- _context2.prev = 9;
161
- _context2.t0 = _context2["catch"](3);
208
+ case 24:
209
+ _context2.prev = 24;
210
+ _context2.t2 = _context2["catch"](3);
162
211
  showAlert({
163
212
  message: t('copyReminderContent.error'),
164
213
  severity: 'error',
165
214
  variant: 'filled'
166
215
  });
167
- log.error("Error in 'copyToClipboard' function when trying to copy to clipboard", _context2.t0);
216
+ log.error("Error in 'copyToClipboard' function when trying to copy to clipboard", _context2.t2);
168
217
 
169
- case 13:
218
+ case 28:
170
219
  case "end":
171
220
  return _context2.stop();
172
221
  }
173
- }, _callee2, null, [[3, 9]]);
222
+ }, _callee2, null, [[3, 24]]);
174
223
  }));
175
224
 
176
225
  return function copyToClipboard(_x2) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cozy-sharing",
3
- "version": "30.2.0",
3
+ "version": "30.2.2",
4
4
  "description": "Provides sharing login for React applications.",
5
5
  "main": "dist/index.js",
6
6
  "author": "Cozy",
@@ -59,7 +59,7 @@
59
59
  "cozy-intent": "^2.31.1",
60
60
  "cozy-minilog": "^3.10.1",
61
61
  "cozy-ui": "^138.1.0",
62
- "cozy-ui-plus": "^7.0.0",
62
+ "cozy-ui-plus": "^7.1.0",
63
63
  "jest": "30.3.0",
64
64
  "jest-environment-jsdom": "30.3.0",
65
65
  "react": "16.12.0",
@@ -84,5 +84,5 @@
84
84
  "sideEffects": [
85
85
  "*.css"
86
86
  ],
87
- "gitHead": "b8142134f35abde2f7c32aa2109f5f5b111505c9"
87
+ "gitHead": "54d377d08003a9d2eb2bd96f9eeb87d27637d841"
88
88
  }
@@ -75,18 +75,29 @@ const ShareByLink = ({
75
75
  }
76
76
 
77
77
  const generateLinkAndCopyLinkToClipboard = async () => {
78
- let linkToCopy = link
79
- const wasNewlyGenerated = !linkToCopy
80
- if (!linkToCopy) {
81
- linkToCopy = await handleGenerateLink()
78
+ if (link) {
79
+ await copyToClipboard(link, { t, showAlert })
80
+ return
82
81
  }
83
- if (linkToCopy) {
84
- await copyToClipboard(linkToCopy, { t, showAlert })
85
- if (wasNewlyGenerated && autoOpenShareRestriction) {
86
- toggleEditDialogOpen()
87
- }
88
- } else {
82
+ // Safari: the clipboard API must be invoked synchronously within the
83
+ // click handler, so we pass the pending link to `copyToClipboard`.
84
+ // The resolved URL is captured here so we can branch on success after
85
+ // the copy has completed, without re-awaiting the promise.
86
+ let generatedLink = null
87
+ await copyToClipboard(
88
+ handleGenerateLink().then(generated => {
89
+ generatedLink = generated
90
+ if (!generated) throw new Error('link-generation-failed')
91
+ return generated
92
+ }),
93
+ { t, showAlert }
94
+ )
95
+ if (!generatedLink) {
89
96
  showGenericError()
97
+ return
98
+ }
99
+ if (autoOpenShareRestriction) {
100
+ toggleEditDialogOpen()
90
101
  }
91
102
  }
92
103
 
@@ -77,51 +77,55 @@ export const ShareRestrictionModal = ({ file, onClose }) => {
77
77
 
78
78
  const handleClick = async () => {
79
79
  setLoading(true)
80
- // If the file is not shared, we create a new sharing link
81
- if (!hasSharingLink) {
82
- const ttl = makeTTL(dateToggle && selectedDate)
83
- const { data: perms } = await createPermissions({
84
- file,
85
- t,
86
- ttl,
87
- password,
88
- editingRights,
89
- documentType,
90
- shareByLink,
91
- showAlert
92
- })
93
- const url = generateWebLink({
94
- cozyUrl: client.getStackClient().uri,
95
- searchParams: [['sharecode', perms.attributes.shortcodes.code]],
96
- pathname: '/public',
97
- slug: getAppSlugFromDocumentType({ documentType }),
98
- subDomainType: client.capabilities.flat_subdomains ? 'flat' : 'nested'
99
- })
100
- await copyToClipboard(url, { t, showAlert })
101
- onClose()
102
- } else {
103
- const [{ data: perms }] = await updatePermissions({
104
- file,
105
- t,
106
- dateToggle,
107
- selectedDate,
108
- passwordToggle,
109
- password,
110
- editingRights,
111
- documentType,
112
- updateDocumentPermissions,
113
- showAlert
114
- })
115
- const url = generateWebLink({
80
+ // The clipboard write must be initiated synchronously within the click
81
+ // handler for Safari to honor the user gesture; we pass a pending blob
82
+ // whose content resolves after the permission round-trip.
83
+ const permsPromise = hasSharingLink
84
+ ? updatePermissions({
85
+ file,
86
+ t,
87
+ dateToggle,
88
+ selectedDate,
89
+ passwordToggle,
90
+ password,
91
+ editingRights,
92
+ documentType,
93
+ updateDocumentPermissions,
94
+ showAlert
95
+ }).then(([{ data: perms }]) => perms)
96
+ : createPermissions({
97
+ file,
98
+ t,
99
+ ttl: makeTTL(dateToggle && selectedDate),
100
+ password,
101
+ editingRights,
102
+ documentType,
103
+ shareByLink,
104
+ showAlert
105
+ }).then(({ data: perms }) => perms)
106
+
107
+ const urlPromise = permsPromise.then(perms =>
108
+ generateWebLink({
116
109
  cozyUrl: client.getStackClient().uri,
117
110
  searchParams: [['sharecode', perms.attributes.shortcodes.code]],
118
111
  pathname: '/public',
119
112
  slug: getAppSlugFromDocumentType({ documentType }),
120
113
  subDomainType: client.capabilities.flat_subdomains ? 'flat' : 'nested'
121
114
  })
122
- await copyToClipboard(url, { t, showAlert })
123
- onClose()
115
+ )
116
+
117
+ await copyToClipboard(urlPromise, { t, showAlert })
118
+ try {
119
+ // Observe the permission result so we can keep the modal open (and
120
+ // preserve the user's date / password / editing-rights inputs) if
121
+ // the permission round-trip failed. copyToClipboard swallows its
122
+ // own errors so the await above always resolves.
123
+ await permsPromise
124
+ } catch {
125
+ setLoading(false)
126
+ return
124
127
  }
128
+ onClose()
125
129
  }
126
130
 
127
131
  const handleRevokeLink = async () => {
@@ -96,17 +96,36 @@ export const generateShareLinkFromFile = ({
96
96
  }
97
97
 
98
98
  /**
99
- * Copy a string to the clipboard
99
+ * Copy a value to the clipboard.
100
100
  *
101
- * @param {string} value - The string saved to the clipboard
101
+ * Accepts a resolved string or a `Promise<string>`. The promise form is
102
+ * required when the value depends on a prior async step (e.g. creating a
103
+ * sharing link) from within a click handler: Safari drops the user-gesture
104
+ * context across any `await`, so `navigator.clipboard.writeText` called after
105
+ * an awaited call throws `NotAllowedError`. Passing a pending blob to
106
+ * `ClipboardItem` lets the clipboard API be invoked synchronously while the
107
+ * content is resolved later.
108
+ *
109
+ * @param {string | Promise<string>} value - Value to copy
102
110
  * @param {object} [options]
103
- * @param {number} options.t - Translation function
111
+ * @param {Function} options.t - Translation function
104
112
  * @param {Function} options.showAlert - Function to display an alert
105
113
  */
106
114
  export const copyToClipboard = async (value, { t, showAlert } = {}) => {
107
115
  if (!value) return false
108
116
  try {
109
- await navigator.clipboard.writeText(value)
117
+ if (typeof value === 'string') {
118
+ await navigator.clipboard.writeText(value)
119
+ } else if (typeof ClipboardItem !== 'undefined') {
120
+ const blobPromise = Promise.resolve(value).then(
121
+ text => new Blob([text], { type: 'text/plain' })
122
+ )
123
+ await navigator.clipboard.write([
124
+ new ClipboardItem({ 'text/plain': blobPromise })
125
+ ])
126
+ } else {
127
+ await navigator.clipboard.writeText(await value)
128
+ }
110
129
  showAlert({
111
130
  message: t('copyReminderContent.success'),
112
131
  severity: 'success',