react-native-signature-canvas 5.0.1 → 5.1.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 +16 -1
- package/README.md +2 -8
- package/h5/html.js +22 -21
- package/h5/js/app.js +108 -134
- package/index.d.ts +7 -8
- package/index.js +120 -44
- package/package.json +5 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,19 @@
|
|
|
1
|
-
# [5.
|
|
1
|
+
# [5.1.0](https://github.com/YanYuanFE/react-native-signature-canvas/compare/v5.0.2...v5.1.0) (2026-07-05)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
* harden string injection, prop handling, and WebView bridge ([98f158d](https://github.com/YanYuanFE/react-native-signature-canvas/commit/98f158d4f883981052b88d07f1b7c3c703cd0401))
|
|
6
|
+
|
|
7
|
+
## [5.0.2](https://github.com/YanYuanFE/react-native-signature-canvas/compare/v5.0.1...v5.0.2) (2025-12-28)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* enhance signature canvas with bottom sheet integration and WebView improvements ([69f84a1](https://github.com/YanYuanFE/react-native-signature-canvas/commit/69f84a1))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## [5.0.1](https://github.com/YanYuanFE/react-native-signature-canvas/compare/v4.5.1...v5.0.1) (2025-12-28)
|
|
2
17
|
|
|
3
18
|
|
|
4
19
|
### Bug Fixes
|
package/README.md
CHANGED
|
@@ -401,12 +401,6 @@ const styles = StyleSheet.create({
|
|
|
401
401
|
|
|
402
402
|
## Performance & Reliability
|
|
403
403
|
|
|
404
|
-
### Automatic Error Recovery
|
|
405
|
-
- **Smart retry logic** with exponential backoff
|
|
406
|
-
- **Circuit breaker pattern** to prevent cascading failures
|
|
407
|
-
- **Memory leak prevention** with automatic cleanup
|
|
408
|
-
- **Performance monitoring** with automatic optimization
|
|
409
|
-
|
|
410
404
|
### Performance Features
|
|
411
405
|
- **Debounced resize handling** for smooth interaction
|
|
412
406
|
- **Memory pressure detection** with adaptive optimization
|
|
@@ -421,7 +415,7 @@ const styles = StyleSheet.create({
|
|
|
421
415
|
|
|
422
416
|
## Migration Guide
|
|
423
417
|
|
|
424
|
-
### From v4.
|
|
418
|
+
### From v4.x to v5.x
|
|
425
419
|
|
|
426
420
|
This version is fully backward compatible. New features:
|
|
427
421
|
|
|
@@ -515,7 +509,7 @@ npm install && npm start
|
|
|
515
509
|
|
|
516
510
|
## Changelog
|
|
517
511
|
|
|
518
|
-
###
|
|
512
|
+
### v5.0.1 (Latest)
|
|
519
513
|
- 🆕 Added `webviewProps` for WebView customization
|
|
520
514
|
- 🆕 Enhanced error handling with automatic recovery
|
|
521
515
|
- 🆕 Performance monitoring and optimization
|
package/h5/html.js
CHANGED
|
@@ -12,36 +12,36 @@ export default (script) =>
|
|
|
12
12
|
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
|
13
13
|
|
|
14
14
|
<style>
|
|
15
|
+
* {
|
|
16
|
+
box-sizing: border-box;
|
|
17
|
+
margin: 0;
|
|
18
|
+
padding: 0;
|
|
19
|
+
}
|
|
20
|
+
html, body {
|
|
21
|
+
width: 100%;
|
|
22
|
+
height: 100%;
|
|
23
|
+
margin: 0;
|
|
24
|
+
padding: 0;
|
|
25
|
+
overflow: hidden;
|
|
26
|
+
}
|
|
15
27
|
body {
|
|
16
28
|
font-family: Helvetica, Sans-Serif;
|
|
17
|
-
|
|
18
29
|
-moz-user-select: none;
|
|
19
30
|
-webkit-user-select: none;
|
|
20
31
|
-ms-user-select: none;
|
|
21
|
-
margin:0;
|
|
22
|
-
overflow:hidden;
|
|
23
|
-
}
|
|
24
|
-
body,html {
|
|
25
|
-
width: 100%;
|
|
26
|
-
height: 300px;
|
|
27
32
|
}
|
|
28
|
-
|
|
29
|
-
box-sizing: border-box;
|
|
30
|
-
margin: 0;
|
|
31
|
-
padding: 0;
|
|
32
|
-
}
|
|
33
|
-
|
|
33
|
+
|
|
34
34
|
.rotated-true {
|
|
35
35
|
transform: rotate(90deg);
|
|
36
36
|
transform-origin:bottom left;
|
|
37
|
-
|
|
37
|
+
|
|
38
38
|
position:absolute;
|
|
39
39
|
top: -100vw;
|
|
40
40
|
left: 0;
|
|
41
|
-
|
|
41
|
+
|
|
42
42
|
height:100vw;
|
|
43
43
|
width:100vh;
|
|
44
|
-
|
|
44
|
+
|
|
45
45
|
overflow:auto;
|
|
46
46
|
}
|
|
47
47
|
.rotated-false {
|
|
@@ -56,15 +56,16 @@ export default (script) =>
|
|
|
56
56
|
background-color: #fff;
|
|
57
57
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.27), 0 0 40px rgba(0, 0, 0, 0.08) inset;
|
|
58
58
|
}
|
|
59
|
-
|
|
59
|
+
|
|
60
60
|
.m-signature-pad--body {
|
|
61
61
|
position: relative;
|
|
62
|
+
width: 100%;
|
|
62
63
|
height: 100%;
|
|
63
64
|
border: 1px solid #f4f4f4;
|
|
64
65
|
}
|
|
65
|
-
|
|
66
|
-
.m-signature-pad--body
|
|
67
|
-
|
|
66
|
+
|
|
67
|
+
.m-signature-pad--body canvas {
|
|
68
|
+
display: block;
|
|
68
69
|
position: absolute;
|
|
69
70
|
left: 0;
|
|
70
71
|
top: 0;
|
|
@@ -125,7 +126,7 @@ export default (script) =>
|
|
|
125
126
|
|
|
126
127
|
@media screen and (min-device-width: 768px) and (max-device-width: 1024px) {
|
|
127
128
|
.m-signature-pad {
|
|
128
|
-
margin:
|
|
129
|
+
margin: 0;
|
|
129
130
|
}
|
|
130
131
|
}
|
|
131
132
|
|
package/h5/js/app.js
CHANGED
|
@@ -1,59 +1,53 @@
|
|
|
1
1
|
export default `
|
|
2
|
-
// Enhanced error handling and validation
|
|
3
2
|
var wrapper = document.getElementById("signature-pad"),
|
|
4
3
|
clearButton = wrapper && wrapper.querySelector("[data-action=clear]"),
|
|
5
4
|
saveButton = wrapper && wrapper.querySelector("[data-action=save]"),
|
|
6
5
|
canvas = wrapper && wrapper.querySelector("canvas"),
|
|
7
6
|
signaturePad;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
clearTimeout(timeout);
|
|
19
|
-
func.apply(this, arguments);
|
|
20
|
-
};
|
|
21
|
-
clearTimeout(timeout);
|
|
22
|
-
timeout = setTimeout(later, wait);
|
|
23
|
-
};
|
|
7
|
+
|
|
8
|
+
// Single bridge entry point. Guards the rare window where the RN WebView
|
|
9
|
+
// bridge is missing (injection race / WebView torn down) and surfaces the
|
|
10
|
+
// dropped message instead of swallowing it.
|
|
11
|
+
function postMessage(data) {
|
|
12
|
+
if (window.ReactNativeWebView && window.ReactNativeWebView.postMessage) {
|
|
13
|
+
window.ReactNativeWebView.postMessage(data);
|
|
14
|
+
} else {
|
|
15
|
+
console.warn("[signature-canvas] RN bridge unavailable, dropped message: " + data);
|
|
16
|
+
}
|
|
24
17
|
}
|
|
25
|
-
|
|
18
|
+
|
|
26
19
|
function resizeCanvas() {
|
|
27
|
-
if (!canvas || !canvas.getContext) {
|
|
28
|
-
console.warn('Canvas not available for resize');
|
|
20
|
+
if (!canvas || !canvas.getContext || !signaturePad) {
|
|
29
21
|
return;
|
|
30
22
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
23
|
+
|
|
24
|
+
var context = canvas.getContext("2d");
|
|
25
|
+
var ratio = Math.max(window.devicePixelRatio || 1, 1);
|
|
26
|
+
|
|
27
|
+
// Save current signature data before resizing
|
|
28
|
+
var imgData = signaturePad.toData();
|
|
29
|
+
var hasDrawnContent = imgData && imgData.length > 0;
|
|
30
|
+
|
|
31
|
+
// Use canvas client dimensions
|
|
32
|
+
var width = canvas.clientWidth;
|
|
33
|
+
var height = canvas.clientHeight;
|
|
34
|
+
|
|
35
|
+
// Resize canvas (this clears the canvas)
|
|
36
|
+
canvas.width = width * ratio;
|
|
37
|
+
canvas.height = height * ratio;
|
|
38
|
+
context.scale(ratio, ratio);
|
|
39
|
+
|
|
40
|
+
// Restore signature content
|
|
41
|
+
if (hasDrawnContent) {
|
|
42
|
+
signaturePad.fromData(imgData);
|
|
43
|
+
} else if (dataURL) {
|
|
44
|
+
signaturePad.fromDataURL(dataURL);
|
|
46
45
|
}
|
|
47
46
|
}
|
|
48
|
-
|
|
49
|
-
// Use debounced resize handler
|
|
50
|
-
var debouncedResize = debounce(resizeCanvas, 100);
|
|
51
|
-
window.addEventListener('resize', debouncedResize);
|
|
52
|
-
resizeCanvas();
|
|
53
|
-
|
|
47
|
+
|
|
54
48
|
signaturePad = new SignaturePad(canvas, {
|
|
55
|
-
onBegin: () =>
|
|
56
|
-
onEnd: () =>
|
|
49
|
+
onBegin: () => postMessage("BEGIN"),
|
|
50
|
+
onEnd: () => postMessage("END"),
|
|
57
51
|
penColor: '<%penColor%>',
|
|
58
52
|
backgroundColor: '<%backgroundColor%>',
|
|
59
53
|
dotSize: <%dotSize%>,
|
|
@@ -62,77 +56,77 @@ export default `
|
|
|
62
56
|
minDistance: <%minDistance%>,
|
|
63
57
|
});
|
|
64
58
|
|
|
59
|
+
// Initial canvas setup
|
|
60
|
+
const observer = new ResizeObserver(() => {
|
|
61
|
+
resizeCanvas();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
observer.observe(canvas);
|
|
65
|
+
|
|
65
66
|
function clearSignature () {
|
|
66
67
|
signaturePad.clear();
|
|
67
|
-
|
|
68
|
+
dataURL='';
|
|
69
|
+
postMessage("CLEAR");
|
|
68
70
|
}
|
|
69
|
-
|
|
71
|
+
|
|
70
72
|
function undo() {
|
|
71
73
|
signaturePad.undo();
|
|
72
|
-
|
|
74
|
+
postMessage("UNDO");
|
|
73
75
|
}
|
|
74
|
-
|
|
76
|
+
|
|
75
77
|
function redo() {
|
|
76
78
|
signaturePad.redo();
|
|
77
|
-
|
|
79
|
+
postMessage("REDO");
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
function changePenColor(color) {
|
|
81
83
|
if (!signaturePad) {
|
|
82
|
-
console.warn('SignaturePad not initialized');
|
|
83
84
|
return;
|
|
84
85
|
}
|
|
85
|
-
|
|
86
86
|
signaturePad.penColor = color;
|
|
87
|
-
|
|
87
|
+
postMessage("CHANGE_PEN");
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
function changePenSize(minW, maxW) {
|
|
91
91
|
if (!signaturePad) {
|
|
92
|
-
console.warn('SignaturePad not initialized');
|
|
93
92
|
return;
|
|
94
93
|
}
|
|
95
|
-
|
|
96
|
-
// Validate numeric values
|
|
97
94
|
if (typeof minW !== 'number' || typeof maxW !== 'number' || minW < 0 || maxW < minW) {
|
|
98
|
-
console.warn('Invalid pen size values:', minW, maxW);
|
|
99
95
|
return;
|
|
100
96
|
}
|
|
101
|
-
|
|
102
97
|
signaturePad.minWidth = minW;
|
|
103
98
|
signaturePad.maxWidth = maxW;
|
|
104
|
-
|
|
99
|
+
postMessage("CHANGE_PEN_SIZE");
|
|
105
100
|
}
|
|
106
|
-
|
|
101
|
+
|
|
107
102
|
function getData () {
|
|
108
103
|
var data = signaturePad.toData();
|
|
109
|
-
|
|
104
|
+
postMessage(JSON.stringify(data));
|
|
110
105
|
}
|
|
111
106
|
|
|
112
|
-
function fromData (pointGroups) {
|
|
113
|
-
signaturePad.fromData(pointGroups);
|
|
114
|
-
|
|
107
|
+
function fromData (pointGroups, suppressClear) {
|
|
108
|
+
signaturePad.fromData(pointGroups, suppressClear);
|
|
109
|
+
postMessage(JSON.stringify(pointGroups));
|
|
115
110
|
}
|
|
116
111
|
|
|
117
112
|
function draw() {
|
|
118
113
|
signaturePad.draw();
|
|
119
|
-
|
|
114
|
+
postMessage("DRAW");
|
|
120
115
|
}
|
|
121
116
|
|
|
122
117
|
function erase() {
|
|
123
118
|
signaturePad.erase();
|
|
124
|
-
|
|
119
|
+
postMessage("ERASE");
|
|
125
120
|
}
|
|
126
121
|
|
|
127
122
|
function cropWhitespace(url) {
|
|
128
123
|
var myImage = new Image();
|
|
129
124
|
myImage.crossOrigin = "Anonymous";
|
|
130
125
|
myImage.onload = function(){
|
|
131
|
-
|
|
126
|
+
postMessage(removeImageBlanks(myImage));
|
|
132
127
|
}
|
|
133
128
|
myImage.src = url;
|
|
134
129
|
|
|
135
|
-
//-----------------------------------------//
|
|
136
130
|
function removeImageBlanks(imageObject) {
|
|
137
131
|
var imgWidth = imageObject.width;
|
|
138
132
|
var imgHeight = imageObject.height;
|
|
@@ -157,49 +151,40 @@ export default `
|
|
|
157
151
|
};
|
|
158
152
|
},
|
|
159
153
|
isWhite = function (rgb) {
|
|
160
|
-
// many images contain noise, as the white is not a pure #fff white
|
|
161
154
|
return !rgb.opacity || (rgb.red > 200 && rgb.green > 200 && rgb.blue > 200);
|
|
162
155
|
},
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
return y;
|
|
175
|
-
} else {
|
|
176
|
-
return Math.min(y + 1, imgHeight);
|
|
156
|
+
scanY = function (fromTop) {
|
|
157
|
+
var offset = fromTop ? 1 : -1;
|
|
158
|
+
for(var y = fromTop ? 0 : imgHeight - 1; fromTop ? (y < imgHeight) : (y > -1); y += offset) {
|
|
159
|
+
for(var x = 0; x < imgWidth; x++) {
|
|
160
|
+
var rgb = getRGB(x, y);
|
|
161
|
+
if (!isWhite(rgb)) {
|
|
162
|
+
if (fromTop) {
|
|
163
|
+
return y;
|
|
164
|
+
} else {
|
|
165
|
+
return Math.min(y + 1, imgHeight);
|
|
166
|
+
}
|
|
177
167
|
}
|
|
178
168
|
}
|
|
179
169
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (fromLeft) {
|
|
194
|
-
return x;
|
|
195
|
-
} else {
|
|
196
|
-
return Math.min(x + 1, imgWidth);
|
|
170
|
+
return null;
|
|
171
|
+
},
|
|
172
|
+
scanX = function (fromLeft) {
|
|
173
|
+
var offset = fromLeft? 1 : -1;
|
|
174
|
+
for(var x = fromLeft ? 0 : imgWidth - 1; fromLeft ? (x < imgWidth) : (x > -1); x += offset) {
|
|
175
|
+
for(var y = 0; y < imgHeight; y++) {
|
|
176
|
+
var rgb = getRGB(x, y);
|
|
177
|
+
if (!isWhite(rgb)) {
|
|
178
|
+
if (fromLeft) {
|
|
179
|
+
return x;
|
|
180
|
+
} else {
|
|
181
|
+
return Math.min(x + 1, imgWidth);
|
|
182
|
+
}
|
|
197
183
|
}
|
|
198
|
-
}
|
|
184
|
+
}
|
|
199
185
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
};
|
|
186
|
+
return null;
|
|
187
|
+
};
|
|
203
188
|
|
|
204
189
|
var cropTop = scanY(true),
|
|
205
190
|
cropBottom = scanY(false),
|
|
@@ -210,7 +195,6 @@ export default `
|
|
|
210
195
|
|
|
211
196
|
canvas.setAttribute("width", cropWidth);
|
|
212
197
|
canvas.setAttribute("height", cropHeight);
|
|
213
|
-
// finally crop the guy
|
|
214
198
|
canvas.getContext("2d").drawImage(imageObject,
|
|
215
199
|
cropLeft, cropTop, cropWidth, cropHeight,
|
|
216
200
|
0, 0, cropWidth, cropHeight);
|
|
@@ -221,37 +205,33 @@ export default `
|
|
|
221
205
|
|
|
222
206
|
function readSignature() {
|
|
223
207
|
if (!signaturePad) {
|
|
224
|
-
console.warn('SignaturePad not initialized');
|
|
225
208
|
return;
|
|
226
209
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
210
|
+
|
|
211
|
+
if (signaturePad.isEmpty()) {
|
|
212
|
+
postMessage("EMPTY");
|
|
213
|
+
} else {
|
|
214
|
+
var imageType = '<%imageType%>' || 'image/png';
|
|
215
|
+
var url = signaturePad.toDataURL(imageType);
|
|
216
|
+
|
|
217
|
+
if (trimWhitespace === true) {
|
|
218
|
+
cropWhitespace(url);
|
|
231
219
|
} else {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
} else {
|
|
238
|
-
window.ReactNativeWebView && window.ReactNativeWebView.postMessage(url);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (autoClear === true && signaturePad) {
|
|
242
|
-
signaturePad.clear();
|
|
243
|
-
}
|
|
220
|
+
postMessage(url);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (autoClear === true && signaturePad) {
|
|
224
|
+
signaturePad.clear();
|
|
244
225
|
}
|
|
245
|
-
} catch (error) {
|
|
246
|
-
console.error('Error reading signature:', error);
|
|
247
226
|
}
|
|
248
227
|
}
|
|
249
228
|
|
|
250
229
|
var autoClear = <%autoClear%>;
|
|
251
|
-
|
|
230
|
+
|
|
252
231
|
var trimWhitespace = <%trimWhitespace%>;
|
|
253
232
|
|
|
254
|
-
|
|
233
|
+
// <%dataURL%> is injected as an already-quoted JSON string literal
|
|
234
|
+
var dataURL = <%dataURL%>;
|
|
255
235
|
|
|
256
236
|
if (dataURL) signaturePad.fromDataURL(dataURL);
|
|
257
237
|
|
|
@@ -259,18 +239,12 @@ export default `
|
|
|
259
239
|
clearButton.addEventListener("click", clearSignature);
|
|
260
240
|
}
|
|
261
241
|
|
|
262
|
-
// Prevent race conditions by sequencing operations
|
|
263
242
|
if (saveButton) {
|
|
264
243
|
saveButton.addEventListener("click", function() {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
getData();
|
|
270
|
-
}, 10);
|
|
271
|
-
} catch (error) {
|
|
272
|
-
console.error('Error in save button click:', error);
|
|
273
|
-
}
|
|
244
|
+
readSignature();
|
|
245
|
+
setTimeout(function() {
|
|
246
|
+
getData();
|
|
247
|
+
}, 10);
|
|
274
248
|
});
|
|
275
249
|
}
|
|
276
250
|
`;
|
package/index.d.ts
CHANGED
|
@@ -5,9 +5,9 @@ declare module "react-native-signature-canvas" {
|
|
|
5
5
|
|
|
6
6
|
// Enhanced type definitions with better error handling
|
|
7
7
|
|
|
8
|
-
type ImageType = "image/png" | "image/jpeg" | "image/svg+xml";
|
|
8
|
+
export type ImageType = "image/png" | "image/jpeg" | "image/svg+xml";
|
|
9
9
|
|
|
10
|
-
type DataURL = string; // Simplified - should be base64 data URL
|
|
10
|
+
export type DataURL = string; // Simplified - should be base64 data URL
|
|
11
11
|
|
|
12
12
|
type ForwardRef<T, P> = React.ForwardRefExoticComponent<
|
|
13
13
|
React.PropsWithoutRef<P> & React.RefAttributes<T>
|
|
@@ -51,7 +51,6 @@ declare module "react-native-signature-canvas" {
|
|
|
51
51
|
onBegin?: EmptyCallback;
|
|
52
52
|
onEnd?: EmptyCallback;
|
|
53
53
|
onLoadEnd?: EmptyCallback;
|
|
54
|
-
onError?: ErrorCallback; // Added missing error callback
|
|
55
54
|
overlayHeight?: number;
|
|
56
55
|
overlayWidth?: number;
|
|
57
56
|
overlaySrc?: string;
|
|
@@ -76,8 +75,11 @@ declare module "react-native-signature-canvas" {
|
|
|
76
75
|
readSignature: () => void;
|
|
77
76
|
undo: () => void;
|
|
78
77
|
redo: () => void;
|
|
79
|
-
fromData: (pointGroups, suppressClear
|
|
80
|
-
|
|
78
|
+
fromData: (pointGroups: any[], suppressClear?: boolean) => void;
|
|
79
|
+
/** Set dataURL without causing WebView reload - useful for restoring signatures */
|
|
80
|
+
setDataURL: (url: string) => void;
|
|
81
|
+
/** Force reinitialize WebView - useful for bottom sheets/modals where WebView state is lost */
|
|
82
|
+
reinitialize: () => void;
|
|
81
83
|
};
|
|
82
84
|
|
|
83
85
|
// Enhanced component interface with better type safety
|
|
@@ -88,7 +90,4 @@ declare module "react-native-signature-canvas" {
|
|
|
88
90
|
|
|
89
91
|
const SignatureView: SignatureCanvasComponent;
|
|
90
92
|
export default SignatureView;
|
|
91
|
-
|
|
92
|
-
// Export additional types for external use
|
|
93
|
-
export { SignatureViewProps, SignatureViewRef, ImageType, DataURL };
|
|
94
93
|
}
|
package/index.js
CHANGED
|
@@ -46,6 +46,12 @@ const styles = StyleSheet.create({
|
|
|
46
46
|
},
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
+
// JSON.stringify produces a valid JS literal with quotes/backslashes/newlines escaped
|
|
50
|
+
const getParamForInjection = (param) =>
|
|
51
|
+
typeof param === "string" || typeof param === "object"
|
|
52
|
+
? JSON.stringify(param)
|
|
53
|
+
: param;
|
|
54
|
+
|
|
49
55
|
const SignatureView = forwardRef(
|
|
50
56
|
(
|
|
51
57
|
{
|
|
@@ -97,63 +103,87 @@ const SignatureView = forwardRef(
|
|
|
97
103
|
ref
|
|
98
104
|
) => {
|
|
99
105
|
const [loading, setLoading] = useState(true);
|
|
100
|
-
|
|
101
106
|
const [hasError, setHasError] = useState(false);
|
|
102
107
|
const [retryCount, setRetryCount] = useState(0);
|
|
108
|
+
// Key to force WebView remount when needed (e.g., after content process termination)
|
|
109
|
+
const [webViewKey, setWebViewKey] = useState(0);
|
|
103
110
|
const maxRetries = 3;
|
|
104
111
|
const webViewRef = useRef();
|
|
112
|
+
// Store dataURL for injection - updates when dataURL prop changes
|
|
113
|
+
const currentDataURLRef = useRef(dataURL);
|
|
114
|
+
|
|
115
|
+
// Update ref when dataURL prop changes
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
currentDataURLRef.current = dataURL;
|
|
118
|
+
}, [dataURL]);
|
|
119
|
+
|
|
105
120
|
// Split source generation for better performance
|
|
121
|
+
// Include webViewKey to regenerate script when WebView needs remounting
|
|
106
122
|
const injectedScript = useMemo(() => {
|
|
123
|
+
// String values go through a function replacer so "$&"-style patterns
|
|
124
|
+
// in user input are inserted literally instead of being expanded
|
|
107
125
|
let script = injectedSignaturePad + injectedApplication;
|
|
108
126
|
script = script.replace(/<%autoClear%>/g, autoClear);
|
|
109
127
|
script = script.replace(/<%trimWhitespace%>/g, trimWhitespace);
|
|
110
|
-
script = script.replace(/<%imageType%>/g, imageType || "image/png");
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
script = script.replace(/<%
|
|
114
|
-
script = script.replace(/<%
|
|
115
|
-
script = script.replace(/<%
|
|
116
|
-
script = script.replace(/<%
|
|
117
|
-
script = script.replace(/<%
|
|
128
|
+
script = script.replace(/<%imageType%>/g, () => imageType || "image/png");
|
|
129
|
+
// Use currentDataURLRef to get the latest dataURL value;
|
|
130
|
+
// JSON.stringify emits a quoted JS string literal with escaping
|
|
131
|
+
script = script.replace(/<%dataURL%>/g, () => JSON.stringify(currentDataURLRef.current || ""));
|
|
132
|
+
script = script.replace(/<%penColor%>/g, () => penColor || "black");
|
|
133
|
+
script = script.replace(/<%backgroundColor%>/g, () => backgroundColor || "rgba(255,255,255,0)");
|
|
134
|
+
script = script.replace(/<%dotSize%>/g, dotSize ?? "null");
|
|
135
|
+
script = script.replace(/<%minWidth%>/g, minWidth ?? 0.5);
|
|
136
|
+
script = script.replace(/<%maxWidth%>/g, maxWidth ?? 2.5);
|
|
137
|
+
script = script.replace(/<%minDistance%>/g, minDistance ?? 5);
|
|
118
138
|
script = script.replace(/<%orientation%>/g, rotated || false);
|
|
119
139
|
return script;
|
|
120
|
-
}, [autoClear, trimWhitespace, imageType,
|
|
140
|
+
}, [autoClear, trimWhitespace, imageType, penColor, backgroundColor, dotSize, minWidth, maxWidth, minDistance, rotated, webViewKey]);
|
|
121
141
|
|
|
122
142
|
const source = useMemo(() => {
|
|
123
143
|
const htmlContentValue = customHtml || htmlContent;
|
|
124
144
|
let html = htmlContentValue(injectedScript);
|
|
125
|
-
html = html.replace(/<%bgWidth%>/g, bgWidth
|
|
126
|
-
html = html.replace(/<%bgHeight%>/g, bgHeight
|
|
127
|
-
html = html.replace(/<%bgSrc%>/g, bgSrc || "null");
|
|
128
|
-
html = html.replace(/<%overlayWidth%>/g, overlayWidth
|
|
129
|
-
html = html.replace(/<%overlayHeight%>/g, overlayHeight
|
|
130
|
-
html = html.replace(/<%overlaySrc%>/g, overlaySrc || "null");
|
|
131
|
-
html = html.replace(/<%style%>/g, webStyle || "");
|
|
132
|
-
html = html.replace(/<%description%>/g, descriptionText || "Sign above");
|
|
133
|
-
html = html.replace(/<%confirm%>/g, confirmText || "Confirm");
|
|
134
|
-
html = html.replace(/<%clear%>/g, clearText || "Clear");
|
|
145
|
+
html = html.replace(/<%bgWidth%>/g, bgWidth ?? 0);
|
|
146
|
+
html = html.replace(/<%bgHeight%>/g, bgHeight ?? 0);
|
|
147
|
+
html = html.replace(/<%bgSrc%>/g, () => bgSrc || "null");
|
|
148
|
+
html = html.replace(/<%overlayWidth%>/g, overlayWidth ?? 0);
|
|
149
|
+
html = html.replace(/<%overlayHeight%>/g, overlayHeight ?? 0);
|
|
150
|
+
html = html.replace(/<%overlaySrc%>/g, () => overlaySrc || "null");
|
|
151
|
+
html = html.replace(/<%style%>/g, () => webStyle || "");
|
|
152
|
+
html = html.replace(/<%description%>/g, () => descriptionText || "Sign above");
|
|
153
|
+
html = html.replace(/<%confirm%>/g, () => confirmText || "Confirm");
|
|
154
|
+
html = html.replace(/<%clear%>/g, () => clearText || "Clear");
|
|
135
155
|
html = html.replace(/<%orientation%>/g, rotated || false);
|
|
136
156
|
|
|
137
157
|
return { html };
|
|
138
158
|
}, [injectedScript, customHtml, bgWidth, bgHeight, bgSrc, overlayWidth, overlayHeight, overlaySrc, webStyle, descriptionText, confirmText, clearText, rotated]);
|
|
139
159
|
|
|
140
|
-
//
|
|
141
|
-
const
|
|
160
|
+
// Handle dataURL changes dynamically without reloading WebView
|
|
161
|
+
const prevDataURLRef = useRef(dataURL);
|
|
142
162
|
|
|
143
163
|
useEffect(() => {
|
|
144
|
-
|
|
145
|
-
|
|
164
|
+
// Skip if dataURL hasn't changed or WebView isn't ready
|
|
165
|
+
if (prevDataURLRef.current === dataURL || !webViewRef.current || loading) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
146
168
|
|
|
147
|
-
|
|
148
|
-
|
|
169
|
+
prevDataURLRef.current = dataURL;
|
|
170
|
+
|
|
171
|
+
// Update dataURL in WebView without full reload
|
|
172
|
+
if (dataURL) {
|
|
173
|
+
const script = `
|
|
174
|
+
dataURL = ${JSON.stringify(dataURL)};
|
|
175
|
+
if (signaturePad && signaturePad.isEmpty()) {
|
|
176
|
+
signaturePad.fromDataURL(dataURL);
|
|
177
|
+
}
|
|
178
|
+
true;
|
|
179
|
+
`;
|
|
149
180
|
try {
|
|
150
|
-
webViewRef.current.
|
|
151
|
-
setShouldReload(false);
|
|
181
|
+
webViewRef.current.injectJavaScript(script);
|
|
152
182
|
} catch (error) {
|
|
153
|
-
console.warn("
|
|
183
|
+
console.warn("Failed to update dataURL:", error);
|
|
154
184
|
}
|
|
155
185
|
}
|
|
156
|
-
}, [
|
|
186
|
+
}, [dataURL, loading]);
|
|
157
187
|
|
|
158
188
|
const isJson = (str) => {
|
|
159
189
|
try {
|
|
@@ -228,7 +258,7 @@ const SignatureView = forwardRef(
|
|
|
228
258
|
|
|
229
259
|
try {
|
|
230
260
|
const script = params.length > 0
|
|
231
|
-
? `${method}(${params.map(
|
|
261
|
+
? `${method}(${params.map(getParamForInjection).join(',')});true;`
|
|
232
262
|
: `${method}();true;`;
|
|
233
263
|
webViewRef.current.injectJavaScript(script);
|
|
234
264
|
} catch (error) {
|
|
@@ -260,12 +290,44 @@ const SignatureView = forwardRef(
|
|
|
260
290
|
executeWebViewMethod('changePenSize', [minW, maxW]);
|
|
261
291
|
},
|
|
262
292
|
getData: () => executeWebViewMethod('getData'),
|
|
263
|
-
fromData: (pointGroups) => {
|
|
293
|
+
fromData: (pointGroups, suppressClear = false) => {
|
|
264
294
|
if (!pointGroups) {
|
|
265
295
|
console.warn('fromData: pointGroups must be an array');
|
|
266
296
|
return;
|
|
267
297
|
}
|
|
268
|
-
executeWebViewMethod('fromData', [pointGroups,
|
|
298
|
+
executeWebViewMethod('fromData', [pointGroups, suppressClear]);
|
|
299
|
+
},
|
|
300
|
+
// New method to set dataURL without causing WebView reload
|
|
301
|
+
setDataURL: (url) => {
|
|
302
|
+
if (typeof url !== 'string') {
|
|
303
|
+
console.warn('setDataURL: url must be a string');
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (!webViewRef.current) {
|
|
307
|
+
console.warn('WebView ref is null when calling setDataURL');
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const script = `
|
|
311
|
+
dataURL = ${JSON.stringify(url)};
|
|
312
|
+
if (signaturePad) {
|
|
313
|
+
signaturePad.clear();
|
|
314
|
+
if (dataURL) {
|
|
315
|
+
signaturePad.fromDataURL(dataURL);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
true;
|
|
319
|
+
`;
|
|
320
|
+
try {
|
|
321
|
+
webViewRef.current.injectJavaScript(script);
|
|
322
|
+
} catch (error) {
|
|
323
|
+
console.error('Error executing setDataURL:', error);
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
// Force reinitialize WebView - useful for bottom sheets/modals where WebView state is lost
|
|
327
|
+
reinitialize: () => {
|
|
328
|
+
setLoading(true);
|
|
329
|
+
setHasError(false);
|
|
330
|
+
setWebViewKey(prev => prev + 1);
|
|
269
331
|
},
|
|
270
332
|
}),
|
|
271
333
|
[executeWebViewMethod]
|
|
@@ -296,6 +358,16 @@ const SignatureView = forwardRef(
|
|
|
296
358
|
}
|
|
297
359
|
}, [onError, retryCount, maxRetries]);
|
|
298
360
|
|
|
361
|
+
// Handle iOS WebView content process termination (WKWebView can be killed when app is backgrounded)
|
|
362
|
+
// This is crucial for bottom sheets and modals where the component stays mounted but WebView is killed
|
|
363
|
+
const handleContentProcessDidTerminate = useCallback(() => {
|
|
364
|
+
console.warn("WebView content process terminated, reinitializing...");
|
|
365
|
+
setLoading(true);
|
|
366
|
+
setHasError(false);
|
|
367
|
+
// Increment key to force WebView remount with fresh JavaScript context
|
|
368
|
+
setWebViewKey(prev => prev + 1);
|
|
369
|
+
}, []);
|
|
370
|
+
|
|
299
371
|
const handleLoadEnd = useCallback(() => {
|
|
300
372
|
setLoading(false);
|
|
301
373
|
setHasError(false);
|
|
@@ -323,19 +395,9 @@ const SignatureView = forwardRef(
|
|
|
323
395
|
return (
|
|
324
396
|
<View style={[styles.webBg, style]}>
|
|
325
397
|
<WebView
|
|
326
|
-
// Core functionality props (cannot be overridden)
|
|
327
|
-
ref={webViewRef}
|
|
328
|
-
source={source}
|
|
329
|
-
onMessage={getSignature}
|
|
330
|
-
onError={renderError}
|
|
331
|
-
onLoadEnd={handleLoadEnd}
|
|
332
|
-
onLoadStart={handleLoadStart}
|
|
333
|
-
onLoadProgress={handleLoadProgress}
|
|
334
|
-
javaScriptEnabled={true}
|
|
335
|
-
useWebKit={true}
|
|
336
398
|
// Default component props (can be overridden by webviewProps)
|
|
337
399
|
bounces={false}
|
|
338
|
-
style={[webviewContainerStyle]}
|
|
400
|
+
style={[{ flex: 1 }, webviewContainerStyle]}
|
|
339
401
|
scrollEnabled={scrollable}
|
|
340
402
|
androidLayerType={androidLayerType}
|
|
341
403
|
androidHardwareAccelerationDisabled={
|
|
@@ -359,6 +421,20 @@ const SignatureView = forwardRef(
|
|
|
359
421
|
startInLoadingState={true}
|
|
360
422
|
// User-provided WebView props (can override defaults but not core functionality)
|
|
361
423
|
{...webviewProps}
|
|
424
|
+
// Core functionality props (cannot be overridden — placed after the spread)
|
|
425
|
+
// Key for forcing remount when WebView needs reinitialization
|
|
426
|
+
key={`signature-webview-${webViewKey}`}
|
|
427
|
+
ref={webViewRef}
|
|
428
|
+
source={source}
|
|
429
|
+
onMessage={getSignature}
|
|
430
|
+
onError={renderError}
|
|
431
|
+
onLoadEnd={handleLoadEnd}
|
|
432
|
+
onLoadStart={handleLoadStart}
|
|
433
|
+
onLoadProgress={handleLoadProgress}
|
|
434
|
+
// Handle iOS WKWebView content process termination (crucial for bottom sheets/modals)
|
|
435
|
+
onContentProcessDidTerminate={handleContentProcessDidTerminate}
|
|
436
|
+
javaScriptEnabled={true}
|
|
437
|
+
useWebKit={true}
|
|
362
438
|
/>
|
|
363
439
|
{(loading || hasError) && (
|
|
364
440
|
<View style={styles.loadingOverlayContainer}>
|
package/package.json
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-signature-canvas",
|
|
3
|
-
"version": "5.0
|
|
3
|
+
"version": "5.1.0",
|
|
4
4
|
"description": "A performant, customizable React Native signature canvas with advanced error handling, WebView optimization, and TypeScript support for iOS, Android, and Expo",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"genlog": "conventional-changelog -p angular -i CHANGELOG.md
|
|
8
|
-
"
|
|
7
|
+
"genlog": "npx -y -p conventional-changelog -p conventional-changelog-angular conventional-changelog -p angular -i CHANGELOG.md",
|
|
8
|
+
"preversion": "npm whoami",
|
|
9
|
+
"version": "npm run genlog && git add CHANGELOG.md",
|
|
10
|
+
"postversion": "git push --follow-tags && npm publish"
|
|
9
11
|
},
|
|
10
12
|
"repository": {
|
|
11
13
|
"type": "git",
|