react-native-signature-canvas 5.0.0 → 5.0.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 +10 -1
- package/README.md +2 -8
- package/h5/html.js +22 -21
- package/h5/js/app.js +79 -128
- package/index.d.ts +5 -2
- package/index.js +93 -15
- package/package.json +2 -2
- package/CLAUDE.md +0 -88
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
## [5.0.2](https://github.com/YanYuanFE/react-native-signature-canvas/compare/v5.0.1...v5.0.2) (2025-12-28)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* enhance signature canvas with bottom sheet integration and WebView improvements ([69f84a1](https://github.com/YanYuanFE/react-native-signature-canvas/commit/69f84a1))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## [5.0.1](https://github.com/YanYuanFE/react-native-signature-canvas/compare/v4.5.1...v5.0.1) (2025-12-28)
|
|
2
11
|
|
|
3
12
|
|
|
4
13
|
### 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,57 +1,39 @@
|
|
|
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
|
-
if (!wrapper || !canvas) {
|
|
10
|
-
console.error('Required DOM elements not found');
|
|
11
|
-
return;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// Enhanced canvas resize with debouncing
|
|
15
|
-
function debounce(func, wait) {
|
|
16
|
-
var timeout;
|
|
17
|
-
return function executedFunction() {
|
|
18
|
-
var later = function() {
|
|
19
|
-
clearTimeout(timeout);
|
|
20
|
-
func.apply(this, arguments);
|
|
21
|
-
};
|
|
22
|
-
clearTimeout(timeout);
|
|
23
|
-
timeout = setTimeout(later, wait);
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
|
|
7
|
+
|
|
27
8
|
function resizeCanvas() {
|
|
28
|
-
if (!canvas || !canvas.getContext) {
|
|
29
|
-
console.warn('Canvas not available for resize');
|
|
9
|
+
if (!canvas || !canvas.getContext || !signaturePad) {
|
|
30
10
|
return;
|
|
31
11
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
12
|
+
|
|
13
|
+
var context = canvas.getContext("2d");
|
|
14
|
+
var ratio = Math.max(window.devicePixelRatio || 1, 1);
|
|
15
|
+
|
|
16
|
+
// Save current signature data before resizing
|
|
17
|
+
var imgData = signaturePad.toData();
|
|
18
|
+
var hasDrawnContent = imgData && imgData.length > 0;
|
|
19
|
+
|
|
20
|
+
// Use canvas client dimensions
|
|
21
|
+
var width = canvas.clientWidth;
|
|
22
|
+
var height = canvas.clientHeight;
|
|
23
|
+
|
|
24
|
+
// Resize canvas (this clears the canvas)
|
|
25
|
+
canvas.width = width * ratio;
|
|
26
|
+
canvas.height = height * ratio;
|
|
27
|
+
context.scale(ratio, ratio);
|
|
28
|
+
|
|
29
|
+
// Restore signature content
|
|
30
|
+
if (hasDrawnContent) {
|
|
31
|
+
signaturePad.fromData(imgData);
|
|
32
|
+
} else if (dataURL) {
|
|
33
|
+
signaturePad.fromDataURL(dataURL);
|
|
47
34
|
}
|
|
48
35
|
}
|
|
49
|
-
|
|
50
|
-
// Use debounced resize handler
|
|
51
|
-
var debouncedResize = debounce(resizeCanvas, 100);
|
|
52
|
-
window.addEventListener('resize', debouncedResize);
|
|
53
|
-
resizeCanvas();
|
|
54
|
-
|
|
36
|
+
|
|
55
37
|
signaturePad = new SignaturePad(canvas, {
|
|
56
38
|
onBegin: () => window.ReactNativeWebView.postMessage("BEGIN"),
|
|
57
39
|
onEnd: () => window.ReactNativeWebView.postMessage("END"),
|
|
@@ -63,16 +45,20 @@ export default `
|
|
|
63
45
|
minDistance: <%minDistance%>,
|
|
64
46
|
});
|
|
65
47
|
|
|
48
|
+
// Initial canvas setup
|
|
49
|
+
resizeCanvas();
|
|
50
|
+
|
|
66
51
|
function clearSignature () {
|
|
67
52
|
signaturePad.clear();
|
|
53
|
+
dataURL='';
|
|
68
54
|
window.ReactNativeWebView.postMessage("CLEAR");
|
|
69
55
|
}
|
|
70
|
-
|
|
56
|
+
|
|
71
57
|
function undo() {
|
|
72
58
|
signaturePad.undo();
|
|
73
59
|
window.ReactNativeWebView.postMessage("UNDO");
|
|
74
60
|
}
|
|
75
|
-
|
|
61
|
+
|
|
76
62
|
function redo() {
|
|
77
63
|
signaturePad.redo();
|
|
78
64
|
window.ReactNativeWebView.postMessage("REDO");
|
|
@@ -80,37 +66,24 @@ export default `
|
|
|
80
66
|
|
|
81
67
|
function changePenColor(color) {
|
|
82
68
|
if (!signaturePad) {
|
|
83
|
-
console.warn('SignaturePad not initialized');
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Validate color format
|
|
88
|
-
if (typeof color !== 'string' || (!color.match(/^#[0-9A-F]{6}$/i) && !color.match(/^rgba?\(/))) {
|
|
89
|
-
console.warn('Invalid color format:', color);
|
|
90
69
|
return;
|
|
91
70
|
}
|
|
92
|
-
|
|
93
71
|
signaturePad.penColor = color;
|
|
94
72
|
window.ReactNativeWebView && window.ReactNativeWebView.postMessage("CHANGE_PEN");
|
|
95
73
|
}
|
|
96
74
|
|
|
97
75
|
function changePenSize(minW, maxW) {
|
|
98
76
|
if (!signaturePad) {
|
|
99
|
-
console.warn('SignaturePad not initialized');
|
|
100
77
|
return;
|
|
101
78
|
}
|
|
102
|
-
|
|
103
|
-
// Validate numeric values
|
|
104
79
|
if (typeof minW !== 'number' || typeof maxW !== 'number' || minW < 0 || maxW < minW) {
|
|
105
|
-
console.warn('Invalid pen size values:', minW, maxW);
|
|
106
80
|
return;
|
|
107
81
|
}
|
|
108
|
-
|
|
109
82
|
signaturePad.minWidth = minW;
|
|
110
83
|
signaturePad.maxWidth = maxW;
|
|
111
84
|
window.ReactNativeWebView && window.ReactNativeWebView.postMessage("CHANGE_PEN_SIZE");
|
|
112
85
|
}
|
|
113
|
-
|
|
86
|
+
|
|
114
87
|
function getData () {
|
|
115
88
|
var data = signaturePad.toData();
|
|
116
89
|
window.ReactNativeWebView.postMessage(JSON.stringify(data));
|
|
@@ -135,11 +108,10 @@ export default `
|
|
|
135
108
|
var myImage = new Image();
|
|
136
109
|
myImage.crossOrigin = "Anonymous";
|
|
137
110
|
myImage.onload = function(){
|
|
138
|
-
window.ReactNativeWebView.postMessage(removeImageBlanks(myImage));
|
|
111
|
+
window.ReactNativeWebView.postMessage(removeImageBlanks(myImage));
|
|
139
112
|
}
|
|
140
113
|
myImage.src = url;
|
|
141
114
|
|
|
142
|
-
//-----------------------------------------//
|
|
143
115
|
function removeImageBlanks(imageObject) {
|
|
144
116
|
var imgWidth = imageObject.width;
|
|
145
117
|
var imgHeight = imageObject.height;
|
|
@@ -164,49 +136,40 @@ export default `
|
|
|
164
136
|
};
|
|
165
137
|
},
|
|
166
138
|
isWhite = function (rgb) {
|
|
167
|
-
// many images contain noise, as the white is not a pure #fff white
|
|
168
139
|
return !rgb.opacity || (rgb.red > 200 && rgb.green > 200 && rgb.blue > 200);
|
|
169
140
|
},
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
return y;
|
|
182
|
-
} else {
|
|
183
|
-
return Math.min(y + 1, imgHeight);
|
|
141
|
+
scanY = function (fromTop) {
|
|
142
|
+
var offset = fromTop ? 1 : -1;
|
|
143
|
+
for(var y = fromTop ? 0 : imgHeight - 1; fromTop ? (y < imgHeight) : (y > -1); y += offset) {
|
|
144
|
+
for(var x = 0; x < imgWidth; x++) {
|
|
145
|
+
var rgb = getRGB(x, y);
|
|
146
|
+
if (!isWhite(rgb)) {
|
|
147
|
+
if (fromTop) {
|
|
148
|
+
return y;
|
|
149
|
+
} else {
|
|
150
|
+
return Math.min(y + 1, imgHeight);
|
|
151
|
+
}
|
|
184
152
|
}
|
|
185
153
|
}
|
|
186
154
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if (fromLeft) {
|
|
201
|
-
return x;
|
|
202
|
-
} else {
|
|
203
|
-
return Math.min(x + 1, imgWidth);
|
|
155
|
+
return null;
|
|
156
|
+
},
|
|
157
|
+
scanX = function (fromLeft) {
|
|
158
|
+
var offset = fromLeft? 1 : -1;
|
|
159
|
+
for(var x = fromLeft ? 0 : imgWidth - 1; fromLeft ? (x < imgWidth) : (x > -1); x += offset) {
|
|
160
|
+
for(var y = 0; y < imgHeight; y++) {
|
|
161
|
+
var rgb = getRGB(x, y);
|
|
162
|
+
if (!isWhite(rgb)) {
|
|
163
|
+
if (fromLeft) {
|
|
164
|
+
return x;
|
|
165
|
+
} else {
|
|
166
|
+
return Math.min(x + 1, imgWidth);
|
|
167
|
+
}
|
|
204
168
|
}
|
|
205
|
-
}
|
|
169
|
+
}
|
|
206
170
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
};
|
|
171
|
+
return null;
|
|
172
|
+
};
|
|
210
173
|
|
|
211
174
|
var cropTop = scanY(true),
|
|
212
175
|
cropBottom = scanY(false),
|
|
@@ -217,7 +180,6 @@ export default `
|
|
|
217
180
|
|
|
218
181
|
canvas.setAttribute("width", cropWidth);
|
|
219
182
|
canvas.setAttribute("height", cropHeight);
|
|
220
|
-
// finally crop the guy
|
|
221
183
|
canvas.getContext("2d").drawImage(imageObject,
|
|
222
184
|
cropLeft, cropTop, cropWidth, cropHeight,
|
|
223
185
|
0, 0, cropWidth, cropHeight);
|
|
@@ -228,34 +190,29 @@ export default `
|
|
|
228
190
|
|
|
229
191
|
function readSignature() {
|
|
230
192
|
if (!signaturePad) {
|
|
231
|
-
console.warn('SignaturePad not initialized');
|
|
232
193
|
return;
|
|
233
194
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
195
|
+
|
|
196
|
+
if (signaturePad.isEmpty()) {
|
|
197
|
+
window.ReactNativeWebView && window.ReactNativeWebView.postMessage("EMPTY");
|
|
198
|
+
} else {
|
|
199
|
+
var imageType = '<%imageType%>' || 'image/png';
|
|
200
|
+
var url = signaturePad.toDataURL(imageType);
|
|
201
|
+
|
|
202
|
+
if (trimWhitespace === true) {
|
|
203
|
+
cropWhitespace(url);
|
|
238
204
|
} else {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
} else {
|
|
245
|
-
window.ReactNativeWebView && window.ReactNativeWebView.postMessage(url);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
if (autoClear === true && signaturePad) {
|
|
249
|
-
signaturePad.clear();
|
|
250
|
-
}
|
|
205
|
+
window.ReactNativeWebView && window.ReactNativeWebView.postMessage(url);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (autoClear === true && signaturePad) {
|
|
209
|
+
signaturePad.clear();
|
|
251
210
|
}
|
|
252
|
-
} catch (error) {
|
|
253
|
-
console.error('Error reading signature:', error);
|
|
254
211
|
}
|
|
255
212
|
}
|
|
256
213
|
|
|
257
214
|
var autoClear = <%autoClear%>;
|
|
258
|
-
|
|
215
|
+
|
|
259
216
|
var trimWhitespace = <%trimWhitespace%>;
|
|
260
217
|
|
|
261
218
|
var dataURL = '<%dataURL%>';
|
|
@@ -266,18 +223,12 @@ export default `
|
|
|
266
223
|
clearButton.addEventListener("click", clearSignature);
|
|
267
224
|
}
|
|
268
225
|
|
|
269
|
-
// Prevent race conditions by sequencing operations
|
|
270
226
|
if (saveButton) {
|
|
271
227
|
saveButton.addEventListener("click", function() {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
getData();
|
|
277
|
-
}, 10);
|
|
278
|
-
} catch (error) {
|
|
279
|
-
console.error('Error in save button click:', error);
|
|
280
|
-
}
|
|
228
|
+
readSignature();
|
|
229
|
+
setTimeout(function() {
|
|
230
|
+
getData();
|
|
231
|
+
}, 10);
|
|
281
232
|
});
|
|
282
233
|
}
|
|
283
234
|
`;
|
package/index.d.ts
CHANGED
|
@@ -76,8 +76,11 @@ declare module "react-native-signature-canvas" {
|
|
|
76
76
|
readSignature: () => void;
|
|
77
77
|
undo: () => void;
|
|
78
78
|
redo: () => void;
|
|
79
|
-
fromData: (pointGroups, suppressClear
|
|
80
|
-
|
|
79
|
+
fromData: (pointGroups: any[], suppressClear?: boolean) => void;
|
|
80
|
+
/** Set dataURL without causing WebView reload - useful for restoring signatures */
|
|
81
|
+
setDataURL: (url: string) => void;
|
|
82
|
+
/** Force reinitialize WebView - useful for bottom sheets/modals where WebView state is lost */
|
|
83
|
+
reinitialize: () => void;
|
|
81
84
|
};
|
|
82
85
|
|
|
83
86
|
// Enhanced component interface with better type safety
|
package/index.js
CHANGED
|
@@ -46,6 +46,17 @@ const styles = StyleSheet.create({
|
|
|
46
46
|
},
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
+
const getParamForInjection = (param) => {
|
|
50
|
+
switch (typeof param) {
|
|
51
|
+
case "string":
|
|
52
|
+
return `'${param}'`
|
|
53
|
+
case "object":
|
|
54
|
+
return JSON.stringify(param)
|
|
55
|
+
default:
|
|
56
|
+
return param
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
49
60
|
const SignatureView = forwardRef(
|
|
50
61
|
(
|
|
51
62
|
{
|
|
@@ -97,18 +108,29 @@ const SignatureView = forwardRef(
|
|
|
97
108
|
ref
|
|
98
109
|
) => {
|
|
99
110
|
const [loading, setLoading] = useState(true);
|
|
100
|
-
|
|
101
111
|
const [hasError, setHasError] = useState(false);
|
|
102
112
|
const [retryCount, setRetryCount] = useState(0);
|
|
113
|
+
// Key to force WebView remount when needed (e.g., after content process termination)
|
|
114
|
+
const [webViewKey, setWebViewKey] = useState(0);
|
|
103
115
|
const maxRetries = 3;
|
|
104
116
|
const webViewRef = useRef();
|
|
117
|
+
// Store dataURL for injection - updates when dataURL prop changes
|
|
118
|
+
const currentDataURLRef = useRef(dataURL);
|
|
119
|
+
|
|
120
|
+
// Update ref when dataURL prop changes
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
currentDataURLRef.current = dataURL;
|
|
123
|
+
}, [dataURL]);
|
|
124
|
+
|
|
105
125
|
// Split source generation for better performance
|
|
126
|
+
// Include webViewKey to regenerate script when WebView needs remounting
|
|
106
127
|
const injectedScript = useMemo(() => {
|
|
107
128
|
let script = injectedSignaturePad + injectedApplication;
|
|
108
129
|
script = script.replace(/<%autoClear%>/g, autoClear);
|
|
109
130
|
script = script.replace(/<%trimWhitespace%>/g, trimWhitespace);
|
|
110
131
|
script = script.replace(/<%imageType%>/g, imageType || "image/png");
|
|
111
|
-
|
|
132
|
+
// Use currentDataURLRef to get the latest dataURL value
|
|
133
|
+
script = script.replace(/<%dataURL%>/g, currentDataURLRef.current || "");
|
|
112
134
|
script = script.replace(/<%penColor%>/g, penColor || "black");
|
|
113
135
|
script = script.replace(/<%backgroundColor%>/g, backgroundColor || "rgba(255,255,255,0)");
|
|
114
136
|
script = script.replace(/<%dotSize%>/g, dotSize || "null");
|
|
@@ -117,7 +139,7 @@ const SignatureView = forwardRef(
|
|
|
117
139
|
script = script.replace(/<%minDistance%>/g, minDistance || 5);
|
|
118
140
|
script = script.replace(/<%orientation%>/g, rotated || false);
|
|
119
141
|
return script;
|
|
120
|
-
}, [autoClear, trimWhitespace, imageType,
|
|
142
|
+
}, [autoClear, trimWhitespace, imageType, penColor, backgroundColor, dotSize, minWidth, maxWidth, minDistance, rotated, webViewKey]);
|
|
121
143
|
|
|
122
144
|
const source = useMemo(() => {
|
|
123
145
|
const htmlContentValue = customHtml || htmlContent;
|
|
@@ -137,23 +159,33 @@ const SignatureView = forwardRef(
|
|
|
137
159
|
return { html };
|
|
138
160
|
}, [injectedScript, customHtml, bgWidth, bgHeight, bgSrc, overlayWidth, overlayHeight, overlaySrc, webStyle, descriptionText, confirmText, clearText, rotated]);
|
|
139
161
|
|
|
140
|
-
//
|
|
141
|
-
const
|
|
162
|
+
// Handle dataURL changes dynamically without reloading WebView
|
|
163
|
+
const prevDataURLRef = useRef(dataURL);
|
|
142
164
|
|
|
143
165
|
useEffect(() => {
|
|
144
|
-
|
|
145
|
-
|
|
166
|
+
// Skip if dataURL hasn't changed or WebView isn't ready
|
|
167
|
+
if (prevDataURLRef.current === dataURL || !webViewRef.current || loading) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
146
170
|
|
|
147
|
-
|
|
148
|
-
|
|
171
|
+
prevDataURLRef.current = dataURL;
|
|
172
|
+
|
|
173
|
+
// Update dataURL in WebView without full reload
|
|
174
|
+
if (dataURL) {
|
|
175
|
+
const script = `
|
|
176
|
+
dataURL = '${dataURL}';
|
|
177
|
+
if (signaturePad && signaturePad.isEmpty()) {
|
|
178
|
+
signaturePad.fromDataURL(dataURL);
|
|
179
|
+
}
|
|
180
|
+
true;
|
|
181
|
+
`;
|
|
149
182
|
try {
|
|
150
|
-
webViewRef.current.
|
|
151
|
-
setShouldReload(false);
|
|
183
|
+
webViewRef.current.injectJavaScript(script);
|
|
152
184
|
} catch (error) {
|
|
153
|
-
console.warn("
|
|
185
|
+
console.warn("Failed to update dataURL:", error);
|
|
154
186
|
}
|
|
155
187
|
}
|
|
156
|
-
}, [
|
|
188
|
+
}, [dataURL, loading]);
|
|
157
189
|
|
|
158
190
|
const isJson = (str) => {
|
|
159
191
|
try {
|
|
@@ -228,7 +260,7 @@ const SignatureView = forwardRef(
|
|
|
228
260
|
|
|
229
261
|
try {
|
|
230
262
|
const script = params.length > 0
|
|
231
|
-
? `${method}(${params.map(
|
|
263
|
+
? `${method}(${params.map(getParamForInjection).join(',')});true;`
|
|
232
264
|
: `${method}();true;`;
|
|
233
265
|
webViewRef.current.injectJavaScript(script);
|
|
234
266
|
} catch (error) {
|
|
@@ -267,6 +299,38 @@ const SignatureView = forwardRef(
|
|
|
267
299
|
}
|
|
268
300
|
executeWebViewMethod('fromData', [pointGroups, false]);
|
|
269
301
|
},
|
|
302
|
+
// New method to set dataURL without causing WebView reload
|
|
303
|
+
setDataURL: (url) => {
|
|
304
|
+
if (typeof url !== 'string') {
|
|
305
|
+
console.warn('setDataURL: url must be a string');
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (!webViewRef.current) {
|
|
309
|
+
console.warn('WebView ref is null when calling setDataURL');
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const script = `
|
|
313
|
+
dataURL = '${url}';
|
|
314
|
+
if (signaturePad) {
|
|
315
|
+
signaturePad.clear();
|
|
316
|
+
if (dataURL) {
|
|
317
|
+
signaturePad.fromDataURL(dataURL);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
true;
|
|
321
|
+
`;
|
|
322
|
+
try {
|
|
323
|
+
webViewRef.current.injectJavaScript(script);
|
|
324
|
+
} catch (error) {
|
|
325
|
+
console.error('Error executing setDataURL:', error);
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
// Force reinitialize WebView - useful for bottom sheets/modals where WebView state is lost
|
|
329
|
+
reinitialize: () => {
|
|
330
|
+
setLoading(true);
|
|
331
|
+
setHasError(false);
|
|
332
|
+
setWebViewKey(prev => prev + 1);
|
|
333
|
+
},
|
|
270
334
|
}),
|
|
271
335
|
[executeWebViewMethod]
|
|
272
336
|
);
|
|
@@ -296,6 +360,16 @@ const SignatureView = forwardRef(
|
|
|
296
360
|
}
|
|
297
361
|
}, [onError, retryCount, maxRetries]);
|
|
298
362
|
|
|
363
|
+
// Handle iOS WebView content process termination (WKWebView can be killed when app is backgrounded)
|
|
364
|
+
// This is crucial for bottom sheets and modals where the component stays mounted but WebView is killed
|
|
365
|
+
const handleContentProcessDidTerminate = useCallback(() => {
|
|
366
|
+
console.warn("WebView content process terminated, reinitializing...");
|
|
367
|
+
setLoading(true);
|
|
368
|
+
setHasError(false);
|
|
369
|
+
// Increment key to force WebView remount with fresh JavaScript context
|
|
370
|
+
setWebViewKey(prev => prev + 1);
|
|
371
|
+
}, []);
|
|
372
|
+
|
|
299
373
|
const handleLoadEnd = useCallback(() => {
|
|
300
374
|
setLoading(false);
|
|
301
375
|
setHasError(false);
|
|
@@ -323,6 +397,8 @@ const SignatureView = forwardRef(
|
|
|
323
397
|
return (
|
|
324
398
|
<View style={[styles.webBg, style]}>
|
|
325
399
|
<WebView
|
|
400
|
+
// Key for forcing remount when WebView needs reinitialization
|
|
401
|
+
key={`signature-webview-${webViewKey}`}
|
|
326
402
|
// Core functionality props (cannot be overridden)
|
|
327
403
|
ref={webViewRef}
|
|
328
404
|
source={source}
|
|
@@ -331,11 +407,13 @@ const SignatureView = forwardRef(
|
|
|
331
407
|
onLoadEnd={handleLoadEnd}
|
|
332
408
|
onLoadStart={handleLoadStart}
|
|
333
409
|
onLoadProgress={handleLoadProgress}
|
|
410
|
+
// Handle iOS WKWebView content process termination (crucial for bottom sheets/modals)
|
|
411
|
+
onContentProcessDidTerminate={handleContentProcessDidTerminate}
|
|
334
412
|
javaScriptEnabled={true}
|
|
335
413
|
useWebKit={true}
|
|
336
414
|
// Default component props (can be overridden by webviewProps)
|
|
337
415
|
bounces={false}
|
|
338
|
-
style={[webviewContainerStyle]}
|
|
416
|
+
style={[{ flex: 1 }, webviewContainerStyle]}
|
|
339
417
|
scrollEnabled={scrollable}
|
|
340
418
|
androidLayerType={androidLayerType}
|
|
341
419
|
androidHardwareAccelerationDisabled={
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-signature-canvas",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.2",
|
|
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 -s
|
|
7
|
+
"genlog": "conventional-changelog -p angular -i CHANGELOG.md -s",
|
|
8
8
|
"postversion": "git push --follow-tags"
|
|
9
9
|
},
|
|
10
10
|
"repository": {
|
package/CLAUDE.md
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
# CLAUDE.md
|
|
2
|
-
|
|
3
|
-
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
-
|
|
5
|
-
## Project Overview
|
|
6
|
-
|
|
7
|
-
This is `react-native-signature-canvas`, a React Native library for capturing signatures and drawing on a canvas. The library provides a WebView-based signature pad component with smooth drawing capabilities across iOS, Android, and Expo.
|
|
8
|
-
|
|
9
|
-
## Core Architecture
|
|
10
|
-
|
|
11
|
-
The library uses a hybrid architecture combining React Native and web technologies:
|
|
12
|
-
|
|
13
|
-
- **Main Component** (`index.js`): The React Native component that wraps a WebView
|
|
14
|
-
- **WebView Content** (`h5/`): HTML, CSS, and JavaScript that runs inside the WebView
|
|
15
|
-
- `html.js`: HTML template with placeholder variables for configuration
|
|
16
|
-
- `js/signature_pad.js`: Core signature pad functionality
|
|
17
|
-
- `js/app.js`: Application logic for handling signature interactions
|
|
18
|
-
- `css/signature-pad.css`: Styling for the signature pad interface
|
|
19
|
-
- **TypeScript Definitions** (`index.d.ts`): Type definitions for props and ref methods
|
|
20
|
-
|
|
21
|
-
### Key Technical Details
|
|
22
|
-
|
|
23
|
-
1. **Communication**: React Native communicates with the WebView through:
|
|
24
|
-
- `injectedJavaScript`: JavaScript code injected into the WebView
|
|
25
|
-
- `onMessage`: Messages sent from WebView to React Native
|
|
26
|
-
- Template variables in HTML (e.g., `<%penColor%>`, `<%bgSrc%>`)
|
|
27
|
-
|
|
28
|
-
2. **Signature Export**: Signatures are captured as base64 data URLs and passed back to React Native via the WebView message bridge
|
|
29
|
-
|
|
30
|
-
3. **Customization**: The component supports extensive customization through props that modify the injected HTML/CSS/JS
|
|
31
|
-
|
|
32
|
-
## Common Development Commands
|
|
33
|
-
|
|
34
|
-
### Example Apps
|
|
35
|
-
The repository contains multiple example applications for different use cases:
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
# React Native CLI app with TypeScript
|
|
39
|
-
cd example/signapp && npm run android
|
|
40
|
-
cd example/signapp && npm run ios
|
|
41
|
-
|
|
42
|
-
# Expo app with modern Expo Router
|
|
43
|
-
cd example/expo-app && npm start
|
|
44
|
-
cd example/expo-app && npm run android
|
|
45
|
-
cd example/expo-app && npm run ios
|
|
46
|
-
|
|
47
|
-
# Basic React Native CLI app
|
|
48
|
-
cd example/exampleApp && npm start
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
### Testing
|
|
52
|
-
```bash
|
|
53
|
-
# Run tests in example apps
|
|
54
|
-
cd example/signapp && npm test
|
|
55
|
-
cd example/expo-app && npm test
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
### Linting
|
|
59
|
-
```bash
|
|
60
|
-
# Lint example apps
|
|
61
|
-
cd example/signapp && npm run lint
|
|
62
|
-
cd example/expo-app && npm run lint
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
## Development Notes
|
|
66
|
-
|
|
67
|
-
### Modifying the Core Component
|
|
68
|
-
- The main component logic is in `index.js`
|
|
69
|
-
- WebView HTML template is in `h5/html.js`
|
|
70
|
-
- JavaScript functionality is split between `h5/js/signature_pad.js` and `h5/js/app.js`
|
|
71
|
-
|
|
72
|
-
### Adding New Features
|
|
73
|
-
1. Add props to the main component interface
|
|
74
|
-
2. Update TypeScript definitions in `index.d.ts`
|
|
75
|
-
3. Modify the HTML template in `h5/html.js` if UI changes are needed
|
|
76
|
-
4. Update the injected JavaScript in `h5/js/app.js` for new functionality
|
|
77
|
-
5. Test across multiple example apps to ensure compatibility
|
|
78
|
-
|
|
79
|
-
### Example App Structure
|
|
80
|
-
- `signapp/`: Full React Native CLI app with TypeScript and Jest testing
|
|
81
|
-
- `expo-app/`: Modern Expo app with Expo Router and TypeScript
|
|
82
|
-
- `exampleApp/`: Basic React Native CLI app
|
|
83
|
-
- `expo-app1/`, `signapp1/`, `sign-app/`, `ts-expo/`: Various other example configurations
|
|
84
|
-
|
|
85
|
-
### Dependencies
|
|
86
|
-
- Main library only has peer dependency on `react-native-webview`
|
|
87
|
-
- Example apps include additional dependencies for testing and development
|
|
88
|
-
- No build process required for the main library (it's a pure React Native component)
|