react-native-signature-canvas 4.7.2 → 5.0.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/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%",
@@ -49,21 +64,23 @@ const SignatureView = forwardRef(
49
64
  imageType = "",
50
65
  minWidth = 0.5,
51
66
  maxWidth = 2.5,
67
+ minDistance = 5,
52
68
  nestedScrollEnabled = false,
53
- showsVerticalScrollIndicator= true,
54
- onOK = () => {},
55
- onEmpty = () => {},
56
- onClear = () => {},
57
- onUndo = () => {},
58
- onRedo = () => {},
59
- onDraw = () => {},
60
- onErase = () => {},
61
- onGetData = () => {},
62
- onChangePenColor = () => {},
63
- onChangePenSize = () => {},
64
- onBegin = () => {},
65
- onEnd = () => {},
66
- onLoadEnd = () => {},
69
+ showsVerticalScrollIndicator = true,
70
+ onOK = () => { },
71
+ onEmpty = () => { },
72
+ onClear = () => { },
73
+ onUndo = () => { },
74
+ onRedo = () => { },
75
+ onDraw = () => { },
76
+ onErase = () => { },
77
+ onGetData = () => { },
78
+ onChangePenColor = () => { },
79
+ onChangePenSize = () => { },
80
+ onBegin = () => { },
81
+ onEnd = () => { },
82
+ onLoadEnd = () => { },
83
+ onError = () => { },
67
84
  overlayHeight = 0,
68
85
  overlayWidth = 0,
69
86
  overlaySrc = null,
@@ -75,81 +92,69 @@ const SignatureView = forwardRef(
75
92
  webStyle = "",
76
93
  webviewContainerStyle = null,
77
94
  androidLayerType = "hardware",
95
+ webviewProps = {},
78
96
  },
79
97
  ref
80
98
  ) => {
81
99
  const [loading, setLoading] = useState(true);
100
+
101
+ const [hasError, setHasError] = useState(false);
102
+ const [retryCount, setRetryCount] = useState(0);
103
+ const maxRetries = 3;
82
104
  const webViewRef = useRef();
83
- const source = useMemo(() => {
84
- let injectedJavaScript = injectedSignaturePad + injectedApplication;
85
- const htmlContentValue = customHtml ? customHtml : htmlContent;
86
- injectedJavaScript = injectedJavaScript.replace(
87
- /<%autoClear%>/g,
88
- autoClear
89
- );
90
- injectedJavaScript = injectedJavaScript.replace(
91
- /<%trimWhitespace%>/g,
92
- trimWhitespace
93
- );
94
- injectedJavaScript = injectedJavaScript.replace(
95
- /<%imageType%>/g,
96
- imageType
97
- );
98
- injectedJavaScript = injectedJavaScript.replace(/<%dataURL%>/g, dataURL);
99
- injectedJavaScript = injectedJavaScript.replace(
100
- /<%penColor%>/g,
101
- penColor
102
- );
103
- injectedJavaScript = injectedJavaScript.replace(
104
- /<%backgroundColor%>/g,
105
- backgroundColor
106
- );
107
- injectedJavaScript = injectedJavaScript.replace(/<%dotSize%>/g, dotSize);
108
- injectedJavaScript = injectedJavaScript.replace(
109
- /<%minWidth%>/g,
110
- minWidth
111
- );
112
- injectedJavaScript = injectedJavaScript.replace(
113
- /<%maxWidth%>/g,
114
- maxWidth
115
- );
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]);
116
121
 
117
- let html = htmlContentValue(injectedJavaScript);
118
- html = html.replace(/<%bgWidth%>/g, bgWidth);
119
- html = html.replace(/<%bgHeight%>/g, bgHeight);
120
- html = html.replace(/<%bgSrc%>/g, bgSrc);
121
- html = html.replace(/<%overlayWidth%>/g, overlayWidth);
122
- html = html.replace(/<%overlayHeight%>/g, overlayHeight);
123
- html = html.replace(/<%overlaySrc%>/g, overlaySrc);
124
- html = html.replace(/<%style%>/g, webStyle);
125
- html = html.replace(/<%description%>/g, descriptionText);
126
- html = html.replace(/<%confirm%>/g, confirmText);
127
- html = html.replace(/<%clear%>/g, clearText);
128
- 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);
129
136
 
130
137
  return { html };
131
- }, [
132
- customHtml,
133
- autoClear,
134
- trimWhitespace,
135
- rotated,
136
- imageType,
137
- webStyle,
138
- descriptionText,
139
- confirmText,
140
- clearText,
141
- dataURL,
142
- bgSrc,
143
- bgWidth,
144
- bgHeight,
145
- ]);
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);
146
142
 
147
143
  useEffect(() => {
148
- if (webViewRef.current) {
149
- webViewRef.current.reload();
150
- }
144
+ setShouldReload(true);
151
145
  }, [source]);
152
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
+
153
158
  const isJson = (str) => {
154
159
  try {
155
160
  JSON.parse(str);
@@ -159,112 +164,176 @@ const SignatureView = forwardRef(
159
164
  return true;
160
165
  };
161
166
 
162
- const getSignature = (e) => {
163
- switch (e.nativeEvent.data) {
164
- case "BEGIN":
165
- onBegin();
166
- break;
167
- case "END":
168
- onEnd();
169
- break;
170
- case "EMPTY":
171
- onEmpty();
172
- break;
173
- case "CLEAR":
174
- onClear();
175
- break;
176
- case "UNDO":
177
- onUndo();
178
- break;
179
- case "REDO":
180
- onRedo();
181
- break;
182
- case "DRAW":
183
- onDraw();
184
- break;
185
- case "ERASE":
186
- onErase();
187
- break;
188
- case "CHANGE_PEN":
189
- onChangePenColor();
190
- break;
191
- case "CHANGE_PEN_SIZE":
192
- onChangePenSize();
193
- break;
194
- default:
195
- isJson(e.nativeEvent.data)
196
- ? onGetData(e.nativeEvent.data)
197
- : 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;
198
172
  }
199
- };
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
+ }, []);
200
238
 
201
239
  useImperativeHandle(
202
240
  ref,
203
241
  () => ({
204
- readSignature: () => {
205
- if (webViewRef.current) {
206
- webViewRef.current.injectJavaScript("readSignature();true;");
207
- }
208
- },
209
- clearSignature: () => {
210
- if (webViewRef.current) {
211
- webViewRef.current.injectJavaScript("clearSignature();true;");
212
- }
213
- },
214
- undo: () => {
215
- if (webViewRef.current) {
216
- webViewRef.current.injectJavaScript("undo();true;");
217
- }
218
- },
219
- redo: () => {
220
- if (webViewRef.current) {
221
- webViewRef.current.injectJavaScript("redo();true;");
222
- }
223
- },
224
- draw: () => {
225
- if (webViewRef.current) {
226
- webViewRef.current.injectJavaScript("draw();true;");
227
- }
228
- },
229
- erase: () => {
230
- if (webViewRef.current) {
231
- webViewRef.current.injectJavaScript("erase();true;");
232
- }
233
- },
242
+ readSignature: () => executeWebViewMethod('readSignature'),
243
+ clearSignature: () => executeWebViewMethod('clearSignature'),
244
+ undo: () => executeWebViewMethod('undo'),
245
+ redo: () => executeWebViewMethod('redo'),
246
+ draw: () => executeWebViewMethod('draw'),
247
+ erase: () => executeWebViewMethod('erase'),
234
248
  changePenColor: (color) => {
235
- if (webViewRef.current) {
236
- webViewRef.current.injectJavaScript(
237
- "changePenColor('" + color + "');true;"
238
- );
249
+ if (typeof color !== 'string') {
250
+ console.warn('changePenColor: color must be a string');
251
+ return;
239
252
  }
253
+ executeWebViewMethod('changePenColor', [color]);
240
254
  },
241
255
  changePenSize: (minW, maxW) => {
242
- if (webViewRef.current) {
243
- webViewRef.current.injectJavaScript(
244
- "changePenSize(" + minW + "," + maxW + ");true;"
245
- );
256
+ if (typeof minW !== 'number' || typeof maxW !== 'number') {
257
+ console.warn('changePenSize: minW and maxW must be numbers');
258
+ return;
246
259
  }
260
+ executeWebViewMethod('changePenSize', [minW, maxW]);
247
261
  },
248
- getData: () => {
249
- if (webViewRef.current) {
250
- 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;
251
267
  }
268
+ executeWebViewMethod('fromData', [pointGroups, false]);
252
269
  },
253
270
  }),
254
- [webViewRef]
271
+ [executeWebViewMethod]
255
272
  );
256
273
 
257
- const renderError = ({ nativeEvent }) =>
258
- console.warn("WebView error: ", nativeEvent);
274
+ const renderError = useCallback(({ nativeEvent }) => {
275
+ console.error("WebView error: ", nativeEvent);
276
+ setHasError(true);
259
277
 
260
- const handleLoadEnd = () => {
261
- setLoading(false);
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]);
298
+
299
+ const handleLoadEnd = useCallback(() => {
300
+ setLoading(false);
301
+ setHasError(false);
302
+ setRetryCount(0);
303
+
304
+ try {
262
305
  onLoadEnd();
263
- }
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
+ }, []);
264
322
 
265
323
  return (
266
324
  <View style={[styles.webBg, style]}>
267
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)
268
337
  bounces={false}
269
338
  style={[webviewContainerStyle]}
270
339
  scrollEnabled={scrollable}
@@ -272,19 +341,34 @@ const SignatureView = forwardRef(
272
341
  androidHardwareAccelerationDisabled={
273
342
  androidHardwareAccelerationDisabled
274
343
  }
275
- ref={webViewRef}
276
- useWebKit={true}
277
- source={source}
278
- onMessage={getSignature}
279
- javaScriptEnabled={true}
280
- onError={renderError}
281
- onLoadEnd={handleLoadEnd}
282
344
  nestedScrollEnabled={nestedScrollEnabled}
283
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}
284
362
  />
285
- {loading && (
363
+ {(loading || hasError) && (
286
364
  <View style={styles.loadingOverlayContainer}>
287
- <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
+ )}
288
372
  </View>
289
373
  )}
290
374
  </View>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "react-native-signature-canvas",
3
- "version": "4.7.2",
4
- "description": "React Native Signature Component based Canvas for Android && IOS && expo",
3
+ "version": "5.0.0",
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",
package/tea.yaml DELETED
@@ -1,6 +0,0 @@
1
- # https://tea.xyz/what-is-this-file
2
- ---
3
- version: 1.0.0
4
- codeOwners:
5
- - '0x5975b662fb1ba6BFEA65aae6eD781a24Ba8CAAc4'
6
- quorum: 1