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 CHANGED
@@ -1,4 +1,19 @@
1
- # [5.0.0](https://github.com/YanYuanFE/react-native-signature-canvas/compare/v4.5.1...v5.0.0) (2025-06-28)
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.6.x to v4.7.x
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
- ### v4.7.x (Latest)
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
- canvas {
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: 10%;
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
- if (!wrapper || !canvas) {
10
- console.error('Required DOM elements not found');
11
- }
12
-
13
- // Enhanced canvas resize with debouncing
14
- function debounce(func, wait) {
15
- var timeout;
16
- return function executedFunction() {
17
- var later = function() {
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
- try {
33
- var context = canvas.getContext("2d");
34
- var imgData = signaturePad ? signaturePad.toData() : null;
35
- var ratio = Math.max(window.devicePixelRatio || 1, 1);
36
-
37
- canvas.width = canvas.offsetWidth * ratio;
38
- canvas.height = canvas.offsetHeight * ratio;
39
- context.scale(ratio, ratio);
40
-
41
- if (imgData && signaturePad) {
42
- signaturePad.fromData(imgData);
43
- }
44
- } catch (error) {
45
- console.error('Error resizing canvas:', error);
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: () => window.ReactNativeWebView.postMessage("BEGIN"),
56
- onEnd: () => window.ReactNativeWebView.postMessage("END"),
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
- window.ReactNativeWebView.postMessage("CLEAR");
68
+ dataURL='';
69
+ postMessage("CLEAR");
68
70
  }
69
-
71
+
70
72
  function undo() {
71
73
  signaturePad.undo();
72
- window.ReactNativeWebView.postMessage("UNDO");
74
+ postMessage("UNDO");
73
75
  }
74
-
76
+
75
77
  function redo() {
76
78
  signaturePad.redo();
77
- window.ReactNativeWebView.postMessage("REDO");
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
- window.ReactNativeWebView && window.ReactNativeWebView.postMessage("CHANGE_PEN");
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
- window.ReactNativeWebView && window.ReactNativeWebView.postMessage("CHANGE_PEN_SIZE");
99
+ postMessage("CHANGE_PEN_SIZE");
105
100
  }
106
-
101
+
107
102
  function getData () {
108
103
  var data = signaturePad.toData();
109
- window.ReactNativeWebView.postMessage(JSON.stringify(data));
104
+ postMessage(JSON.stringify(data));
110
105
  }
111
106
 
112
- function fromData (pointGroups) {
113
- signaturePad.fromData(pointGroups);
114
- window.ReactNativeWebView.postMessage(JSON.stringify(pointGroups));
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
- window.ReactNativeWebView.postMessage("DRAW");
114
+ postMessage("DRAW");
120
115
  }
121
116
 
122
117
  function erase() {
123
118
  signaturePad.erase();
124
- window.ReactNativeWebView.postMessage("ERASE");
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
- window.ReactNativeWebView.postMessage(removeImageBlanks(myImage)); //Will return cropped image data
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
- scanY = function (fromTop) {
164
- var offset = fromTop ? 1 : -1;
165
-
166
- // loop through each row
167
- for(var y = fromTop ? 0 : imgHeight - 1; fromTop ? (y < imgHeight) : (y > -1); y += offset) {
168
-
169
- // loop through each column
170
- for(var x = 0; x < imgWidth; x++) {
171
- var rgb = getRGB(x, y);
172
- if (!isWhite(rgb)) {
173
- if (fromTop) {
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
- return null; // all image is white
182
- },
183
- scanX = function (fromLeft) {
184
- var offset = fromLeft? 1 : -1;
185
-
186
- // loop through each column
187
- for(var x = fromLeft ? 0 : imgWidth - 1; fromLeft ? (x < imgWidth) : (x > -1); x += offset) {
188
-
189
- // loop through each row
190
- for(var y = 0; y < imgHeight; y++) {
191
- var rgb = getRGB(x, y);
192
- if (!isWhite(rgb)) {
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
- return null; // all image is white
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
- try {
229
- if (signaturePad.isEmpty()) {
230
- window.ReactNativeWebView && window.ReactNativeWebView.postMessage("EMPTY");
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
- var imageType = '<%imageType%>' || 'image/png';
233
- var url = signaturePad.toDataURL(imageType);
234
-
235
- if (trimWhitespace === true) {
236
- cropWhitespace(url);
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
- var dataURL = '<%dataURL%>';
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
- try {
266
- readSignature();
267
- // Small delay to prevent race condition
268
- setTimeout(function() {
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 = false) => void;
80
- // Removed cropWhitespace as it's not exposed in the component
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
- script = script.replace(/<%dataURL%>/g, dataURL || "");
112
- script = script.replace(/<%penColor%>/g, penColor || "black");
113
- script = script.replace(/<%backgroundColor%>/g, backgroundColor || "rgba(255,255,255,0)");
114
- script = script.replace(/<%dotSize%>/g, dotSize || "null");
115
- script = script.replace(/<%minWidth%>/g, minWidth || 0.5);
116
- script = script.replace(/<%maxWidth%>/g, maxWidth || 2.5);
117
- script = script.replace(/<%minDistance%>/g, minDistance || 5);
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, dataURL, penColor, backgroundColor, dotSize, minWidth, maxWidth, minDistance, rotated]);
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 || 0);
126
- html = html.replace(/<%bgHeight%>/g, bgHeight || 0);
127
- html = html.replace(/<%bgSrc%>/g, bgSrc || "null");
128
- html = html.replace(/<%overlayWidth%>/g, overlayWidth || 0);
129
- html = html.replace(/<%overlayHeight%>/g, overlayHeight || 0);
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
- // Optimize WebView reload to prevent excessive reloads
141
- const [shouldReload, setShouldReload] = useState(false);
160
+ // Handle dataURL changes dynamically without reloading WebView
161
+ const prevDataURLRef = useRef(dataURL);
142
162
 
143
163
  useEffect(() => {
144
- setShouldReload(true);
145
- }, [source]);
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
- useEffect(() => {
148
- if (shouldReload && webViewRef.current) {
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.reload();
151
- setShouldReload(false);
181
+ webViewRef.current.injectJavaScript(script);
152
182
  } catch (error) {
153
- console.warn("WebView reload failed:", error);
183
+ console.warn("Failed to update dataURL:", error);
154
184
  }
155
185
  }
156
- }, [shouldReload]);
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(p => typeof p === 'string' ? `'${p}'` : p).join(',')});true;`
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, false]);
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.1",
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 -s -r 0",
8
- "postversion": "git push --follow-tags"
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",