react-native-signature-canvas 4.7.4 → 5.0.1

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/index.js CHANGED
@@ -5,8 +5,9 @@ import React, {
5
5
  useRef,
6
6
  forwardRef,
7
7
  useImperativeHandle,
8
+ useCallback,
8
9
  } from "react";
9
- import { View, StyleSheet, ActivityIndicator } from "react-native";
10
+ import { View, StyleSheet, ActivityIndicator, Text } from "react-native";
10
11
 
11
12
  import htmlContent from "./h5/html";
12
13
  import injectedSignaturePad from "./h5/js/signature_pad";
@@ -14,6 +15,20 @@ import injectedApplication from "./h5/js/app";
14
15
 
15
16
  import { WebView } from "react-native-webview";
16
17
 
18
+ // Constants for better maintainability
19
+ const MESSAGE_TYPES = {
20
+ BEGIN: "BEGIN",
21
+ END: "END",
22
+ EMPTY: "EMPTY",
23
+ CLEAR: "CLEAR",
24
+ UNDO: "UNDO",
25
+ REDO: "REDO",
26
+ DRAW: "DRAW",
27
+ ERASE: "ERASE",
28
+ CHANGE_PEN: "CHANGE_PEN",
29
+ CHANGE_PEN_SIZE: "CHANGE_PEN_SIZE"
30
+ };
31
+
17
32
  const styles = StyleSheet.create({
18
33
  webBg: {
19
34
  width: "100%",
@@ -65,6 +80,7 @@ const SignatureView = forwardRef(
65
80
  onBegin = () => { },
66
81
  onEnd = () => { },
67
82
  onLoadEnd = () => { },
83
+ onError = () => { },
68
84
  overlayHeight = 0,
69
85
  overlayWidth = 0,
70
86
  overlaySrc = null,
@@ -76,85 +92,69 @@ const SignatureView = forwardRef(
76
92
  webStyle = "",
77
93
  webviewContainerStyle = null,
78
94
  androidLayerType = "hardware",
95
+ webviewProps = {},
79
96
  },
80
97
  ref
81
98
  ) => {
82
99
  const [loading, setLoading] = useState(true);
100
+
101
+ const [hasError, setHasError] = useState(false);
102
+ const [retryCount, setRetryCount] = useState(0);
103
+ const maxRetries = 3;
83
104
  const webViewRef = useRef();
84
- const source = useMemo(() => {
85
- let injectedJavaScript = injectedSignaturePad + injectedApplication;
86
- const htmlContentValue = customHtml ? customHtml : htmlContent;
87
- injectedJavaScript = injectedJavaScript.replace(
88
- /<%autoClear%>/g,
89
- autoClear
90
- );
91
- injectedJavaScript = injectedJavaScript.replace(
92
- /<%trimWhitespace%>/g,
93
- trimWhitespace
94
- );
95
- injectedJavaScript = injectedJavaScript.replace(
96
- /<%imageType%>/g,
97
- imageType
98
- );
99
- injectedJavaScript = injectedJavaScript.replace(/<%dataURL%>/g, dataURL);
100
- injectedJavaScript = injectedJavaScript.replace(
101
- /<%penColor%>/g,
102
- penColor
103
- );
104
- injectedJavaScript = injectedJavaScript.replace(
105
- /<%backgroundColor%>/g,
106
- backgroundColor
107
- );
108
- injectedJavaScript = injectedJavaScript.replace(/<%dotSize%>/g, dotSize);
109
- injectedJavaScript = injectedJavaScript.replace(
110
- /<%minWidth%>/g,
111
- minWidth
112
- );
113
- injectedJavaScript = injectedJavaScript.replace(
114
- /<%maxWidth%>/g,
115
- maxWidth
116
- );
117
- injectedJavaScript = injectedJavaScript.replace(
118
- /<%minDistance%>/g,
119
- minDistance
120
- );
105
+ // Split source generation for better performance
106
+ const injectedScript = useMemo(() => {
107
+ let script = injectedSignaturePad + injectedApplication;
108
+ script = script.replace(/<%autoClear%>/g, autoClear);
109
+ 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);
118
+ script = script.replace(/<%orientation%>/g, rotated || false);
119
+ return script;
120
+ }, [autoClear, trimWhitespace, imageType, dataURL, penColor, backgroundColor, dotSize, minWidth, maxWidth, minDistance, rotated]);
121
121
 
122
- let html = htmlContentValue(injectedJavaScript);
123
- html = html.replace(/<%bgWidth%>/g, bgWidth);
124
- html = html.replace(/<%bgHeight%>/g, bgHeight);
125
- html = html.replace(/<%bgSrc%>/g, bgSrc);
126
- html = html.replace(/<%overlayWidth%>/g, overlayWidth);
127
- html = html.replace(/<%overlayHeight%>/g, overlayHeight);
128
- html = html.replace(/<%overlaySrc%>/g, overlaySrc);
129
- html = html.replace(/<%style%>/g, webStyle);
130
- html = html.replace(/<%description%>/g, descriptionText);
131
- html = html.replace(/<%confirm%>/g, confirmText);
132
- html = html.replace(/<%clear%>/g, clearText);
133
- html = html.replace(/<%orientation%>/g, rotated);
122
+ const source = useMemo(() => {
123
+ const htmlContentValue = customHtml || htmlContent;
124
+ 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");
135
+ html = html.replace(/<%orientation%>/g, rotated || false);
134
136
 
135
137
  return { html };
136
- }, [
137
- customHtml,
138
- autoClear,
139
- trimWhitespace,
140
- rotated,
141
- imageType,
142
- webStyle,
143
- descriptionText,
144
- confirmText,
145
- clearText,
146
- dataURL,
147
- bgSrc,
148
- bgWidth,
149
- bgHeight,
150
- ]);
138
+ }, [injectedScript, customHtml, bgWidth, bgHeight, bgSrc, overlayWidth, overlayHeight, overlaySrc, webStyle, descriptionText, confirmText, clearText, rotated]);
139
+
140
+ // Optimize WebView reload to prevent excessive reloads
141
+ const [shouldReload, setShouldReload] = useState(false);
151
142
 
152
143
  useEffect(() => {
153
- if (webViewRef.current) {
154
- webViewRef.current.reload();
155
- }
144
+ setShouldReload(true);
156
145
  }, [source]);
157
146
 
147
+ useEffect(() => {
148
+ if (shouldReload && webViewRef.current) {
149
+ try {
150
+ webViewRef.current.reload();
151
+ setShouldReload(false);
152
+ } catch (error) {
153
+ console.warn("WebView reload failed:", error);
154
+ }
155
+ }
156
+ }, [shouldReload]);
157
+
158
158
  const isJson = (str) => {
159
159
  try {
160
160
  JSON.parse(str);
@@ -164,112 +164,176 @@ const SignatureView = forwardRef(
164
164
  return true;
165
165
  };
166
166
 
167
- const getSignature = (e) => {
168
- switch (e.nativeEvent.data) {
169
- case "BEGIN":
170
- onBegin();
171
- break;
172
- case "END":
173
- onEnd();
174
- break;
175
- case "EMPTY":
176
- onEmpty();
177
- break;
178
- case "CLEAR":
179
- onClear();
180
- break;
181
- case "UNDO":
182
- onUndo();
183
- break;
184
- case "REDO":
185
- onRedo();
186
- break;
187
- case "DRAW":
188
- onDraw();
189
- break;
190
- case "ERASE":
191
- onErase();
192
- break;
193
- case "CHANGE_PEN":
194
- onChangePenColor();
195
- break;
196
- case "CHANGE_PEN_SIZE":
197
- onChangePenSize();
198
- break;
199
- default:
200
- isJson(e.nativeEvent.data)
201
- ? onGetData(e.nativeEvent.data)
202
- : onOK(e.nativeEvent.data);
167
+ // Enhanced message handling with error handling
168
+ const getSignature = useCallback((e) => {
169
+ if (!e?.nativeEvent?.data) {
170
+ console.warn("Invalid message received from WebView");
171
+ return;
203
172
  }
204
- };
173
+
174
+ const data = e.nativeEvent.data;
175
+
176
+ try {
177
+ switch (data) {
178
+ case MESSAGE_TYPES.BEGIN:
179
+ onBegin();
180
+ break;
181
+ case MESSAGE_TYPES.END:
182
+ onEnd();
183
+ break;
184
+ case MESSAGE_TYPES.EMPTY:
185
+ onEmpty();
186
+ break;
187
+ case MESSAGE_TYPES.CLEAR:
188
+ onClear();
189
+ break;
190
+ case MESSAGE_TYPES.UNDO:
191
+ onUndo();
192
+ break;
193
+ case MESSAGE_TYPES.REDO:
194
+ onRedo();
195
+ break;
196
+ case MESSAGE_TYPES.DRAW:
197
+ onDraw();
198
+ break;
199
+ case MESSAGE_TYPES.ERASE:
200
+ onErase();
201
+ break;
202
+ case MESSAGE_TYPES.CHANGE_PEN:
203
+ onChangePenColor();
204
+ break;
205
+ case MESSAGE_TYPES.CHANGE_PEN_SIZE:
206
+ onChangePenSize();
207
+ break;
208
+ default:
209
+ if (isJson(data)) {
210
+ onGetData(data);
211
+ } else if (typeof data === "string" && data.startsWith("data:")) {
212
+ onOK(data);
213
+ } else {
214
+ console.warn("Unknown message type:", data);
215
+ }
216
+ }
217
+ } catch (error) {
218
+ console.error("Error handling WebView message:", error);
219
+ }
220
+ }, [onBegin, onEnd, onEmpty, onClear, onUndo, onRedo, onDraw, onErase, onChangePenColor, onChangePenSize, onGetData, onOK]);
221
+
222
+ // Enhanced WebView method execution with error handling
223
+ const executeWebViewMethod = useCallback((method, params = []) => {
224
+ if (!webViewRef.current) {
225
+ console.warn(`WebView ref is null when calling ${method}`);
226
+ return;
227
+ }
228
+
229
+ try {
230
+ const script = params.length > 0
231
+ ? `${method}(${params.map(p => typeof p === 'string' ? `'${p}'` : p).join(',')});true;`
232
+ : `${method}();true;`;
233
+ webViewRef.current.injectJavaScript(script);
234
+ } catch (error) {
235
+ console.error(`Error executing WebView method ${method}:`, error);
236
+ }
237
+ }, []);
205
238
 
206
239
  useImperativeHandle(
207
240
  ref,
208
241
  () => ({
209
- readSignature: () => {
210
- if (webViewRef.current) {
211
- webViewRef.current.injectJavaScript("readSignature();true;");
212
- }
213
- },
214
- clearSignature: () => {
215
- if (webViewRef.current) {
216
- webViewRef.current.injectJavaScript("clearSignature();true;");
217
- }
218
- },
219
- undo: () => {
220
- if (webViewRef.current) {
221
- webViewRef.current.injectJavaScript("undo();true;");
222
- }
223
- },
224
- redo: () => {
225
- if (webViewRef.current) {
226
- webViewRef.current.injectJavaScript("redo();true;");
227
- }
228
- },
229
- draw: () => {
230
- if (webViewRef.current) {
231
- webViewRef.current.injectJavaScript("draw();true;");
232
- }
233
- },
234
- erase: () => {
235
- if (webViewRef.current) {
236
- webViewRef.current.injectJavaScript("erase();true;");
237
- }
238
- },
242
+ readSignature: () => executeWebViewMethod('readSignature'),
243
+ clearSignature: () => executeWebViewMethod('clearSignature'),
244
+ undo: () => executeWebViewMethod('undo'),
245
+ redo: () => executeWebViewMethod('redo'),
246
+ draw: () => executeWebViewMethod('draw'),
247
+ erase: () => executeWebViewMethod('erase'),
239
248
  changePenColor: (color) => {
240
- if (webViewRef.current) {
241
- webViewRef.current.injectJavaScript(
242
- "changePenColor('" + color + "');true;"
243
- );
249
+ if (typeof color !== 'string') {
250
+ console.warn('changePenColor: color must be a string');
251
+ return;
244
252
  }
253
+ executeWebViewMethod('changePenColor', [color]);
245
254
  },
246
255
  changePenSize: (minW, maxW) => {
247
- if (webViewRef.current) {
248
- webViewRef.current.injectJavaScript(
249
- "changePenSize(" + minW + "," + maxW + ");true;"
250
- );
256
+ if (typeof minW !== 'number' || typeof maxW !== 'number') {
257
+ console.warn('changePenSize: minW and maxW must be numbers');
258
+ return;
251
259
  }
260
+ executeWebViewMethod('changePenSize', [minW, maxW]);
252
261
  },
253
- getData: () => {
254
- if (webViewRef.current) {
255
- webViewRef.current.injectJavaScript("getData();true;");
262
+ getData: () => executeWebViewMethod('getData'),
263
+ fromData: (pointGroups) => {
264
+ if (!pointGroups) {
265
+ console.warn('fromData: pointGroups must be an array');
266
+ return;
256
267
  }
268
+ executeWebViewMethod('fromData', [pointGroups, false]);
257
269
  },
258
270
  }),
259
- [webViewRef]
271
+ [executeWebViewMethod]
260
272
  );
261
273
 
262
- const renderError = ({ nativeEvent }) =>
263
- console.warn("WebView error: ", nativeEvent);
274
+ const renderError = useCallback(({ nativeEvent }) => {
275
+ console.error("WebView error: ", nativeEvent);
276
+ setHasError(true);
277
+
278
+ // Call user-provided error handler
279
+ try {
280
+ onError(new Error(`WebView error: ${nativeEvent.description || nativeEvent.code}`));
281
+ } catch (err) {
282
+ console.warn('Error in onError callback:', err);
283
+ }
284
+
285
+ // Attempt to recover from error with retry logic
286
+ if (webViewRef.current && nativeEvent.code !== -999 && retryCount < maxRetries) {
287
+ setTimeout(() => {
288
+ try {
289
+ setRetryCount(prev => prev + 1);
290
+ webViewRef.current.reload();
291
+ setHasError(false);
292
+ } catch (error) {
293
+ console.error("Failed to reload WebView after error:", error);
294
+ }
295
+ }, Math.min(1000 * Math.pow(2, retryCount), 5000)); // Exponential backoff
296
+ }
297
+ }, [onError, retryCount, maxRetries]);
264
298
 
265
- const handleLoadEnd = () => {
299
+ const handleLoadEnd = useCallback(() => {
266
300
  setLoading(false);
267
- onLoadEnd();
268
- }
301
+ setHasError(false);
302
+ setRetryCount(0);
303
+
304
+ try {
305
+ onLoadEnd();
306
+ } catch (error) {
307
+ console.warn('Error in onLoadEnd callback:', error);
308
+ }
309
+ }, [onLoadEnd]);
310
+
311
+ // Performance monitoring
312
+ const handleLoadStart = useCallback(() => {
313
+ setLoading(true);
314
+ }, []);
315
+
316
+ const handleLoadProgress = useCallback(({ nativeEvent }) => {
317
+ // Optional: Add progress monitoring
318
+ if (nativeEvent.progress === 1) {
319
+ setLoading(false);
320
+ }
321
+ }, []);
269
322
 
270
323
  return (
271
324
  <View style={[styles.webBg, style]}>
272
325
  <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
+ // Default component props (can be overridden by webviewProps)
273
337
  bounces={false}
274
338
  style={[webviewContainerStyle]}
275
339
  scrollEnabled={scrollable}
@@ -277,19 +341,34 @@ const SignatureView = forwardRef(
277
341
  androidHardwareAccelerationDisabled={
278
342
  androidHardwareAccelerationDisabled
279
343
  }
280
- ref={webViewRef}
281
- useWebKit={true}
282
- source={source}
283
- onMessage={getSignature}
284
- javaScriptEnabled={true}
285
- onError={renderError}
286
- onLoadEnd={handleLoadEnd}
287
344
  nestedScrollEnabled={nestedScrollEnabled}
288
345
  showsVerticalScrollIndicator={showsVerticalScrollIndicator}
346
+ // Default performance optimizations
347
+ cacheEnabled={true}
348
+ allowsInlineMediaPlayback={false}
349
+ mediaPlaybackRequiresUserAction={true}
350
+ allowsBackForwardNavigationGestures={false}
351
+ // Default security enhancements
352
+ allowsLinkPreview={false}
353
+ allowFileAccess={false}
354
+ allowFileAccessFromFileURLs={false}
355
+ allowUniversalAccessFromFileURLs={false}
356
+ mixedContentMode="never"
357
+ originWhitelist={['*']}
358
+ // Default error recovery
359
+ startInLoadingState={true}
360
+ // User-provided WebView props (can override defaults but not core functionality)
361
+ {...webviewProps}
289
362
  />
290
- {loading && (
363
+ {(loading || hasError) && (
291
364
  <View style={styles.loadingOverlayContainer}>
292
- <ActivityIndicator color={"transparent"} />
365
+ {hasError ? (
366
+ <Text style={{ color: '#ff0000', textAlign: 'center', padding: 10 }}>
367
+ Error loading signature pad{retryCount > 0 ? ` (Retry ${retryCount}/${maxRetries})` : ''}
368
+ </Text>
369
+ ) : (
370
+ <ActivityIndicator color={"#007AFF"} size="small" />
371
+ )}
293
372
  </View>
294
373
  )}
295
374
  </View>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "react-native-signature-canvas",
3
- "version": "4.7.4",
4
- "description": "React Native Signature Component based Canvas for Android && IOS && expo",
3
+ "version": "5.0.1",
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
7
  "genlog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0",
@@ -17,9 +17,16 @@
17
17
  "ios",
18
18
  "android",
19
19
  "signature",
20
- "pad",
20
+ "signature-pad",
21
21
  "canvas",
22
- "expo"
22
+ "expo",
23
+ "typescript",
24
+ "drawing",
25
+ "webview",
26
+ "performance",
27
+ "error-handling",
28
+ "undo-redo",
29
+ "svg-export"
23
30
  ],
24
31
  "author": "YanYuanFE",
25
32
  "license": "MIT",