rnnovaui 0.10.21 → 0.10.23

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rnnovaui",
3
- "version": "0.10.21",
3
+ "version": "0.10.23",
4
4
  "description": "",
5
5
  "main": "src/index.js",
6
6
  "react-native": "src/index.js",
@@ -47,6 +47,7 @@
47
47
  "react": ">=19.0.0",
48
48
  "react-native": ">=0.76.0",
49
49
  "react-native-reanimated": ">=3.0.0",
50
- "react-native-safe-area-context": ">=4.0.0"
50
+ "react-native-safe-area-context": ">=4.0.0",
51
+ "react-native-svg": ">=15.0.0"
51
52
  }
52
53
  }
@@ -0,0 +1,1216 @@
1
+ import React, {
2
+ forwardRef,
3
+ useCallback,
4
+ useImperativeHandle,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ } from "react";
9
+
10
+ import { PanResponder, StyleSheet, Text, View, Image } from "react-native";
11
+
12
+ import Svg, {
13
+ Defs,
14
+ G,
15
+ LinearGradient as SvgLinearGradient,
16
+ Mask,
17
+ Path,
18
+ Rect,
19
+ Stop,
20
+ } from "react-native-svg";
21
+
22
+ import Animated, {
23
+ FadeIn,
24
+ useAnimatedStyle,
25
+ useSharedValue,
26
+ withSequence,
27
+ withSpring,
28
+ withTiming,
29
+ } from "react-native-reanimated";
30
+
31
+ import { Feather, Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
32
+
33
+ /*
34
+ * Use the SAME theme hook that your UIProvider already exposes.
35
+ */
36
+ import { useTheme } from "../../theme";
37
+
38
+ /* =========================================================
39
+ DEFAULTS
40
+ ========================================================= */
41
+
42
+ const DEFAULT_WIDTH = 350;
43
+ const DEFAULT_HEIGHT = 220;
44
+
45
+ /* =========================================================
46
+ BUILT-IN THEMES
47
+ ========================================================= */
48
+
49
+ const THEMES = {
50
+ classic: {
51
+ background: ["#4214C7", "#6B20E8"],
52
+ border: "#8B6BFF",
53
+
54
+ title: "#FFFFFF",
55
+ subtitle: "#EDE7FF",
56
+ accent: "#FFD42A",
57
+
58
+ scratch: ["#D9DEE5", "#BEC6D0"],
59
+ scratchBorder: "#FFFFFF",
60
+ scratchText: "#263248",
61
+ scratchIcon: "#263248",
62
+
63
+ icon: "gift",
64
+ iconType: "material",
65
+ iconColor: "#E8D5FF",
66
+
67
+ titleLines: ["Scratch", "& Win"],
68
+ titleAccentLine: 1,
69
+
70
+ subtitleText: "Reveal your reward!",
71
+ scratchText: "Scratch Here",
72
+ },
73
+
74
+ trophy: {
75
+ background: ["#15110A", "#3D2A0A"],
76
+ border: "#D6A72C",
77
+
78
+ title: "#FFFFFF",
79
+ subtitle: "#F5E5B1",
80
+ accent: "#FFD43B",
81
+
82
+ scratch: ["#E1E5EA", "#C1C8D1"],
83
+ scratchBorder: "#E7C84A",
84
+ scratchText: "#273043",
85
+ scratchIcon: "#273043",
86
+
87
+ icon: "trophy",
88
+ iconType: "material",
89
+ iconColor: "#FFC62B",
90
+
91
+ titleLines: ["LUCKY", "SCRATCH"],
92
+ titleAccentLine: 0,
93
+
94
+ subtitleText: "Win Exciting Prizes!",
95
+ scratchText: "Scratch Here",
96
+ },
97
+
98
+ gift: {
99
+ background: ["#E51B18", "#C80F13"],
100
+ border: "#FF6B61",
101
+
102
+ title: "#FFFFFF",
103
+ subtitle: "#FFF0EA",
104
+ accent: "#FFD42A",
105
+
106
+ scratch: ["#E1E5EA", "#C1C8D1"],
107
+ scratchBorder: "#FFFFFF",
108
+ scratchText: "#273043",
109
+ scratchIcon: "#273043",
110
+
111
+ icon: "gift",
112
+ iconType: "material",
113
+ iconColor: "#FFD43B",
114
+
115
+ titleLines: ["Scratch", "& Win"],
116
+ titleAccentLine: 1,
117
+
118
+ subtitleText: "Big Prizes Await!",
119
+ scratchText: "Scratch Here",
120
+ },
121
+
122
+ travel: {
123
+ background: ["#10B8A7", "#12A9D2"],
124
+ border: "#6FE7DF",
125
+
126
+ title: "#FFFFFF",
127
+ subtitle: "#E7FFFF",
128
+ accent: "#D7FFFF",
129
+
130
+ scratch: ["#E1E5EA", "#C1C8D1"],
131
+ scratchBorder: "#FFFFFF",
132
+ scratchText: "#273043",
133
+ scratchIcon: "#273043",
134
+
135
+ icon: "airplane",
136
+ iconType: "ion",
137
+ iconColor: "#FFFFFF",
138
+
139
+ titleLines: ["TRAVEL", "LUCK"],
140
+ titleAccentLine: null,
141
+
142
+ subtitleText: "Scratch & Get Your Reward",
143
+ scratchText: "Scratch Here",
144
+ },
145
+
146
+ discount: {
147
+ background: ["#F62992", "#A91DDB"],
148
+ border: "#FF8ED0",
149
+
150
+ title: "#FFFFFF",
151
+ subtitle: "#FFE8F6",
152
+ accent: "#FFE32E",
153
+
154
+ scratch: ["#E1E5EA", "#C1C8D1"],
155
+ scratchBorder: "#FFFFFF",
156
+ scratchText: "#273043",
157
+ scratchIcon: "#273043",
158
+
159
+ icon: "percent",
160
+ iconType: "material",
161
+ iconColor: "#FFD52C",
162
+
163
+ titleLines: ["Special", "DISCOUNT"],
164
+ titleAccentLine: 1,
165
+
166
+ subtitleText: "Scratch & Save More!",
167
+ scratchText: "Scratch Here",
168
+ },
169
+
170
+ space: {
171
+ background: ["#06317D", "#0756C9"],
172
+ border: "#3D8DFF",
173
+
174
+ title: "#FFFFFF",
175
+ subtitle: "#DDEBFF",
176
+ accent: "#55D8FF",
177
+
178
+ scratch: ["#E1E5EA", "#C1C8D1"],
179
+ scratchBorder: "#FFFFFF",
180
+ scratchText: "#273043",
181
+ scratchIcon: "#273043",
182
+
183
+ icon: "rocket",
184
+ iconType: "material",
185
+ iconColor: "#FF775C",
186
+
187
+ titleLines: ["WIN", "BIG"],
188
+ titleAccentLine: null,
189
+
190
+ subtitleText: "Scratch & Claim Your Prize!",
191
+ scratchText: "Scratch Here",
192
+ },
193
+
194
+ premium: {
195
+ background: ["#080808", "#222222"],
196
+ border: "#C89B3C",
197
+
198
+ title: "#F5D67B",
199
+ subtitle: "#F5E7BC",
200
+ accent: "#E2B94E",
201
+
202
+ scratch: ["#E1E5EA", "#C1C8D1"],
203
+ scratchBorder: "#D8B454",
204
+ scratchText: "#273043",
205
+ scratchIcon: "#273043",
206
+
207
+ icon: "crown",
208
+ iconType: "material",
209
+ iconColor: "#E3B943",
210
+
211
+ titleLines: ["PREMIUM", "SCRATCH CARD"],
212
+ titleAccentLine: null,
213
+
214
+ subtitleText: "Exclusive Rewards Inside",
215
+ scratchText: "Scratch Here",
216
+ },
217
+
218
+ christmas: {
219
+ background: ["#0874D1", "#16A7E7"],
220
+ border: "#72D9FF",
221
+
222
+ title: "#FFFFFF",
223
+ subtitle: "#E4F7FF",
224
+ accent: "#FFFFFF",
225
+
226
+ scratch: ["#E1E5EA", "#C1C8D1"],
227
+ scratchBorder: "#FFFFFF",
228
+ scratchText: "#273043",
229
+ scratchIcon: "#273043",
230
+
231
+ icon: "snowman",
232
+ iconType: "material",
233
+ iconColor: "#FFFFFF",
234
+
235
+ titleLines: ["Holiday", "SURPRISE"],
236
+ titleAccentLine: null,
237
+
238
+ subtitleText: "Scratch & Win Gifts",
239
+ scratchText: "Scratch Here",
240
+ },
241
+
242
+ mystery: {
243
+ background: ["#B58BFF", "#35BFFF"],
244
+ border: "#E3D5FF",
245
+
246
+ title: "#FFFFFF",
247
+ subtitle: "#F8F2FF",
248
+ accent: "#FF3A96",
249
+
250
+ scratch: ["#E1E5EA", "#C1C8D1"],
251
+ scratchBorder: "#FFFFFF",
252
+ scratchText: "#273043",
253
+ scratchIcon: "#273043",
254
+
255
+ icon: "help-circle",
256
+ iconType: "feather",
257
+ iconColor: "#6424C8",
258
+
259
+ titleLines: ["Mystery", "Scratch"],
260
+ titleAccentLine: null,
261
+
262
+ subtitleText: "What Will You Get?",
263
+ scratchText: "Scratch Here",
264
+ },
265
+ };
266
+
267
+ /* =========================================================
268
+ LABELS
269
+ ========================================================= */
270
+
271
+ const VARIANT_LABELS = {
272
+ classic: "1. Classic Reward",
273
+ trophy: "2. Trophy Theme",
274
+ gift: "3. Gift Theme",
275
+ travel: "4. Travel Theme",
276
+ discount: "5. Discount Theme",
277
+ space: "6. Space Theme",
278
+ premium: "7. Premium Theme",
279
+ christmas: "8. Christmas Theme",
280
+ mystery: "9. Mystery Theme",
281
+ };
282
+
283
+ /* =========================================================
284
+ THEME ICON
285
+ ========================================================= */
286
+
287
+ function getThemeIcon(theme, size = 70) {
288
+ if (theme.iconType === "ion") {
289
+ return <Ionicons name="airplane" size={size} color={theme.iconColor} />;
290
+ }
291
+
292
+ if (theme.iconType === "feather") {
293
+ return <Feather name="help-circle" size={size} color={theme.iconColor} />;
294
+ }
295
+
296
+ const materialIcons = {
297
+ gift: "gift",
298
+ trophy: "trophy",
299
+ percent: "percent",
300
+ rocket: "rocket-launch",
301
+ crown: "crown",
302
+ snowman: "snowman",
303
+ };
304
+
305
+ return (
306
+ <MaterialCommunityIcons
307
+ name={materialIcons[theme.icon] || "gift"}
308
+ size={size}
309
+ color={theme.iconColor}
310
+ />
311
+ );
312
+ }
313
+
314
+ /* =========================================================
315
+ SCRATCH SURFACE
316
+ ========================================================= */
317
+
318
+ function ScratchSurface({
319
+ width,
320
+ height,
321
+ colors,
322
+ borderColor,
323
+ scratchText,
324
+ iconColor,
325
+ threshold,
326
+ revealed,
327
+ onProgress,
328
+ onReveal,
329
+ style,
330
+ }) {
331
+ const [paths, setPaths] = useState([]);
332
+ const [currentPath, setCurrentPath] = useState("");
333
+
334
+ const lastPoint = useRef(null);
335
+ const scratchedDistance = useRef(0);
336
+ const progressRef = useRef(0);
337
+
338
+ const calculateProgress = useCallback(
339
+ (distance) => {
340
+ scratchedDistance.current += distance;
341
+
342
+ /*
343
+ * Estimate scratch coverage from
344
+ * accumulated gesture distance.
345
+ */
346
+ const estimated =
347
+ scratchedDistance.current / Math.max(width * height * 0.75, 1);
348
+
349
+ const nextProgress = Math.min(100, Math.round(estimated * 100));
350
+
351
+ if (nextProgress > progressRef.current) {
352
+ progressRef.current = nextProgress;
353
+
354
+ onProgress?.(nextProgress);
355
+ }
356
+
357
+ if (nextProgress >= threshold && !revealed) {
358
+ onReveal?.();
359
+ }
360
+ },
361
+ [height, onProgress, onReveal, threshold, width, revealed],
362
+ );
363
+
364
+ const scratchAt = useCallback(
365
+ (x, y) => {
366
+ const point = `${x.toFixed(1)},${y.toFixed(1)}`;
367
+
368
+ setCurrentPath((previous) => {
369
+ if (!previous) {
370
+ return `M ${point}`;
371
+ }
372
+
373
+ return `${previous} L ${point}`;
374
+ });
375
+
376
+ if (lastPoint.current) {
377
+ const dx = x - lastPoint.current.x;
378
+
379
+ const dy = y - lastPoint.current.y;
380
+
381
+ const distance = Math.sqrt(dx * dx + dy * dy);
382
+
383
+ calculateProgress(distance);
384
+ }
385
+
386
+ lastPoint.current = {
387
+ x,
388
+ y,
389
+ };
390
+ },
391
+ [calculateProgress],
392
+ );
393
+
394
+ const finishScratch = useCallback(() => {
395
+ if (currentPath) {
396
+ setPaths((previous) => [...previous, currentPath]);
397
+ }
398
+
399
+ setCurrentPath("");
400
+ lastPoint.current = null;
401
+ }, [currentPath]);
402
+
403
+ const panResponder = useMemo(
404
+ () =>
405
+ PanResponder.create({
406
+ onStartShouldSetPanResponder: () => !revealed,
407
+
408
+ onMoveShouldSetPanResponder: () => !revealed,
409
+
410
+ onPanResponderGrant: (event) => {
411
+ const { locationX, locationY } = event.nativeEvent;
412
+
413
+ scratchAt(locationX, locationY);
414
+ },
415
+
416
+ onPanResponderMove: (event) => {
417
+ const { locationX, locationY } = event.nativeEvent;
418
+
419
+ scratchAt(locationX, locationY);
420
+ },
421
+
422
+ onPanResponderRelease: finishScratch,
423
+
424
+ onPanResponderTerminate: finishScratch,
425
+ }),
426
+ [finishScratch, revealed, scratchAt],
427
+ );
428
+
429
+ if (revealed) {
430
+ return null;
431
+ }
432
+
433
+ return (
434
+ <View
435
+ {...panResponder.panHandlers}
436
+ style={[
437
+ styles.scratchSurface,
438
+ {
439
+ width,
440
+ height,
441
+ borderColor,
442
+ },
443
+ style,
444
+ ]}
445
+ >
446
+ <Svg width={width} height={height} pointerEvents="none">
447
+ <Defs>
448
+ <SvgLinearGradient
449
+ id="scratchSurfaceGradient"
450
+ x1="0"
451
+ y1="0"
452
+ x2="1"
453
+ y2="1"
454
+ >
455
+ <Stop offset="0" stopColor={colors[0]} />
456
+
457
+ <Stop offset="0.5" stopColor={colors[1]} />
458
+
459
+ <Stop offset="1" stopColor={colors[0]} />
460
+ </SvgLinearGradient>
461
+
462
+ <Mask id="scratchMask">
463
+ <Rect x="0" y="0" width={width} height={height} fill="white" />
464
+
465
+ {paths.map((path, index) => (
466
+ <Path
467
+ key={`path-${index}`}
468
+ d={path}
469
+ stroke="black"
470
+ strokeWidth={38}
471
+ strokeLinecap="round"
472
+ strokeLinejoin="round"
473
+ fill="none"
474
+ />
475
+ ))}
476
+
477
+ {currentPath ? (
478
+ <Path
479
+ d={currentPath}
480
+ stroke="black"
481
+ strokeWidth={38}
482
+ strokeLinecap="round"
483
+ strokeLinejoin="round"
484
+ fill="none"
485
+ />
486
+ ) : null}
487
+ </Mask>
488
+ </Defs>
489
+
490
+ <G mask="url(#scratchMask)">
491
+ <Rect
492
+ x="0"
493
+ y="0"
494
+ width={width}
495
+ height={height}
496
+ fill="url(#scratchSurfaceGradient)"
497
+ />
498
+
499
+ {/* Scratch texture */}
500
+ {Array.from({
501
+ length: 15,
502
+ }).map((_, index) => (
503
+ <Path
504
+ key={`texture-a-${index}`}
505
+ d={`M ${-30 + index * 34} ${height}
506
+ L ${60 + index * 34} 0`}
507
+ stroke="#FFFFFF"
508
+ strokeOpacity={0.18}
509
+ strokeWidth={1}
510
+ />
511
+ ))}
512
+
513
+ {Array.from({
514
+ length: 8,
515
+ }).map((_, index) => (
516
+ <Path
517
+ key={`texture-b-${index}`}
518
+ d={`M ${index * 55} 0
519
+ L ${index * 55 + 85} ${height}`}
520
+ stroke="#6D7783"
521
+ strokeOpacity={0.08}
522
+ strokeWidth={1}
523
+ />
524
+ ))}
525
+ </G>
526
+ </Svg>
527
+
528
+ {/* Scratch instruction */}
529
+ <View pointerEvents="none" style={styles.scratchPrompt}>
530
+ <MaterialCommunityIcons
531
+ name="gesture-tap"
532
+ size={30}
533
+ color={iconColor}
534
+ />
535
+
536
+ <Text
537
+ style={[
538
+ styles.scratchPromptText,
539
+ {
540
+ color: iconColor,
541
+ },
542
+ ]}
543
+ >
544
+ {scratchText}
545
+ </Text>
546
+ </View>
547
+ </View>
548
+ );
549
+ }
550
+
551
+ /* =========================================================
552
+ MAIN COMPONENT
553
+ ========================================================= */
554
+
555
+ const UIScratchCard = forwardRef(
556
+ (
557
+ {
558
+ /*
559
+ * Built-in design
560
+ */
561
+ variant = "classic",
562
+
563
+ /*
564
+ * Dimensions
565
+ */
566
+ width = DEFAULT_WIDTH,
567
+ height = DEFAULT_HEIGHT,
568
+
569
+ /*
570
+ * Content
571
+ */
572
+ title,
573
+ subtitle,
574
+ scratchText,
575
+
576
+ /*
577
+ * Reward
578
+ */
579
+ reward,
580
+ rewardIcon,
581
+
582
+ /*
583
+ * Main illustration
584
+ */
585
+ icon,
586
+ image,
587
+
588
+ /*
589
+ * Scratch
590
+ */
591
+ scratchThreshold = 55,
592
+
593
+ /*
594
+ * Reanimated
595
+ */
596
+ reanimated = true,
597
+
598
+ /*
599
+ * Events
600
+ */
601
+ onProgress,
602
+ onReveal,
603
+ onPress,
604
+
605
+ /*
606
+ * Background override
607
+ */
608
+ backgroundColors,
609
+ backgroundColor,
610
+
611
+ /*
612
+ * Styling
613
+ */
614
+ borderRadius = 16,
615
+
616
+ cardStyle,
617
+ contentStyle,
618
+ titleStyle,
619
+ subtitleStyle,
620
+ scratchStyle,
621
+ rewardStyle,
622
+ imageStyle,
623
+ iconContainerStyle,
624
+
625
+ /*
626
+ * Optional custom content
627
+ */
628
+ children,
629
+
630
+ /*
631
+ * Disable interaction
632
+ */
633
+ disabled = false,
634
+ },
635
+ ref,
636
+ ) => {
637
+ /*
638
+ * Theme from UIProvider.
639
+ */
640
+ const { theme } = useTheme();
641
+
642
+ const builtInTheme = THEMES[variant] || THEMES.classic;
643
+
644
+ /*
645
+ * Revealed state.
646
+ */
647
+ const [revealed, setRevealed] = useState(false);
648
+
649
+ /*
650
+ * Reanimated values.
651
+ */
652
+ const rewardScale = useSharedValue(1);
653
+
654
+ const progressValue = useSharedValue(0);
655
+
656
+ /*
657
+ * Resolve colors.
658
+ *
659
+ * Custom background has priority.
660
+ */
661
+ const resolvedBackground = backgroundColors || builtInTheme.background;
662
+
663
+ /*
664
+ * Theme-aware fallback.
665
+ */
666
+ const themeText = theme?.colors?.text?.primary || "#111827";
667
+
668
+ /*
669
+ * Resolve title.
670
+ */
671
+ const resolvedTitle = title || builtInTheme.titleLines;
672
+
673
+ /*
674
+ * Resolve subtitle.
675
+ */
676
+ const resolvedSubtitle = subtitle || builtInTheme.subtitleText;
677
+
678
+ /*
679
+ * Resolve scratch text.
680
+ */
681
+ const resolvedScratchText = scratchText || builtInTheme.scratchText;
682
+
683
+ /*
684
+ * Reveal.
685
+ */
686
+ const reveal = useCallback(() => {
687
+ if (revealed) {
688
+ return;
689
+ }
690
+
691
+ setRevealed(true);
692
+
693
+ if (reanimated) {
694
+ rewardScale.value = withSequence(
695
+ withTiming(0.92, {
696
+ duration: 100,
697
+ }),
698
+ withSpring(1),
699
+ );
700
+ }
701
+
702
+ onReveal?.({
703
+ variant,
704
+ reward,
705
+ });
706
+ }, [onReveal, reanimated, revealed, reward, rewardScale, variant]);
707
+
708
+ /*
709
+ * Reset.
710
+ */
711
+ const reset = useCallback(() => {
712
+ setRevealed(false);
713
+
714
+ progressValue.value = 0;
715
+
716
+ rewardScale.value = 1;
717
+ }, [progressValue, rewardScale]);
718
+
719
+ /*
720
+ * Public ref API.
721
+ */
722
+ useImperativeHandle(
723
+ ref,
724
+ () => ({
725
+ reveal,
726
+ reset,
727
+ isRevealed: revealed,
728
+ }),
729
+ [reveal, reset, revealed],
730
+ );
731
+
732
+ /*
733
+ * Scratch progress.
734
+ */
735
+ const handleProgress = useCallback(
736
+ (progress) => {
737
+ progressValue.value = withTiming(progress, {
738
+ duration: 100,
739
+ });
740
+
741
+ onProgress?.(progress);
742
+ },
743
+ [onProgress, progressValue],
744
+ );
745
+
746
+ /*
747
+ * Reward animation.
748
+ */
749
+ const rewardAnimatedStyle = useAnimatedStyle(() => ({
750
+ transform: [
751
+ {
752
+ scale: rewardScale.value,
753
+ },
754
+ ],
755
+ }));
756
+
757
+ /*
758
+ * Press handler.
759
+ */
760
+ const handlePress = useCallback(() => {
761
+ onPress?.({
762
+ variant,
763
+ reward,
764
+ revealed,
765
+ });
766
+ }, [onPress, reward, revealed, variant]);
767
+
768
+ return (
769
+ <View
770
+ style={[
771
+ styles.wrapper,
772
+ {
773
+ width,
774
+ },
775
+ ]}
776
+ >
777
+ <View
778
+ style={[
779
+ styles.card,
780
+ {
781
+ width,
782
+ height,
783
+ borderRadius,
784
+ borderColor: builtInTheme.border,
785
+ backgroundColor: backgroundColor || resolvedBackground[0],
786
+ },
787
+ cardStyle,
788
+ ]}
789
+ >
790
+ {/* =========================================
791
+ BACKGROUND
792
+ ========================================= */}
793
+
794
+ <Svg
795
+ pointerEvents="none"
796
+ width={width}
797
+ height={height}
798
+ style={StyleSheet.absoluteFill}
799
+ >
800
+ <Defs>
801
+ <SvgLinearGradient
802
+ id={`scratchCardGradient-${variant}`}
803
+ x1="0"
804
+ y1="0"
805
+ x2="1"
806
+ y2="1"
807
+ >
808
+ <Stop offset="0" stopColor={resolvedBackground[0]} />
809
+
810
+ <Stop
811
+ offset="1"
812
+ stopColor={resolvedBackground[1] || resolvedBackground[0]}
813
+ />
814
+ </SvgLinearGradient>
815
+ </Defs>
816
+
817
+ <Rect
818
+ x="0"
819
+ y="0"
820
+ width={width}
821
+ height={height}
822
+ rx={borderRadius}
823
+ fill={
824
+ backgroundColor
825
+ ? backgroundColor
826
+ : `url(#scratchCardGradient-${variant})`
827
+ }
828
+ />
829
+ </Svg>
830
+
831
+ {/* =========================================
832
+ DECORATIVE ELEMENTS
833
+ ========================================= */}
834
+
835
+ <View pointerEvents="none" style={styles.decorations}>
836
+ <Text
837
+ style={[
838
+ styles.decorationStar,
839
+ {
840
+ color: builtInTheme.accent,
841
+ },
842
+ ]}
843
+ >
844
+
845
+ </Text>
846
+
847
+ <Text
848
+ style={[
849
+ styles.decorationStar,
850
+ styles.decorationStarTwo,
851
+ {
852
+ color: builtInTheme.accent,
853
+ },
854
+ ]}
855
+ >
856
+
857
+ </Text>
858
+
859
+ <Text
860
+ style={[
861
+ styles.decorationStar,
862
+ styles.decorationStarThree,
863
+ {
864
+ color: builtInTheme.accent,
865
+ },
866
+ ]}
867
+ >
868
+
869
+ </Text>
870
+
871
+ <Text
872
+ style={[
873
+ styles.decorationStar,
874
+ styles.decorationStarFour,
875
+ {
876
+ color: builtInTheme.accent,
877
+ },
878
+ ]}
879
+ >
880
+
881
+ </Text>
882
+ </View>
883
+
884
+ {/* =========================================
885
+ HEADER / TITLE AREA
886
+ ========================================= */}
887
+
888
+ <View style={[styles.content, contentStyle]}>
889
+ <View style={styles.textArea}>
890
+ <Text
891
+ style={[
892
+ styles.title,
893
+ {
894
+ color: builtInTheme.title,
895
+ },
896
+ titleStyle,
897
+ ]}
898
+ >
899
+ {Array.isArray(resolvedTitle)
900
+ ? resolvedTitle.map((line, index) => (
901
+ <Text
902
+ key={`${line}-${index}`}
903
+ style={{
904
+ color:
905
+ index === builtInTheme.titleAccentLine
906
+ ? builtInTheme.accent
907
+ : builtInTheme.title,
908
+ }}
909
+ >
910
+ {line}
911
+
912
+ {index < resolvedTitle.length - 1 ? "\n" : ""}
913
+ </Text>
914
+ ))
915
+ : resolvedTitle}
916
+ </Text>
917
+
918
+ <Text
919
+ style={[
920
+ styles.subtitle,
921
+ {
922
+ color: builtInTheme.subtitle,
923
+ },
924
+ subtitleStyle,
925
+ ]}
926
+ >
927
+ {resolvedSubtitle}
928
+ </Text>
929
+ </View>
930
+
931
+ {/* =====================================
932
+ IMAGE / ICON
933
+ ===================================== */}
934
+
935
+ <View style={[styles.visualArea, iconContainerStyle]}>
936
+ {image ? (
937
+ typeof image === "function" ? (
938
+ image({
939
+ theme: builtInTheme,
940
+ uiTheme: theme,
941
+ })
942
+ ) : (
943
+ <Image
944
+ source={image}
945
+ resizeMode="contain"
946
+ style={[styles.visualImage, imageStyle]}
947
+ />
948
+ )
949
+ ) : (
950
+ icon || <ThemeVisualIcon theme={builtInTheme} />
951
+ )}
952
+ </View>
953
+ </View>
954
+
955
+ {/* =========================================
956
+ SCRATCH AREA
957
+ ========================================= */}
958
+
959
+ <View
960
+ style={[
961
+ styles.scratchWrapper,
962
+ {
963
+ left: 22,
964
+ right: 22,
965
+ bottom: 15,
966
+ height: Math.min(82, height * 0.38),
967
+ },
968
+ ]}
969
+ >
970
+ {revealed ? (
971
+ <Animated.View
972
+ entering={reanimated ? FadeIn.duration(250) : undefined}
973
+ style={[
974
+ styles.rewardContainer,
975
+ {
976
+ borderColor: builtInTheme.scratchBorder,
977
+ borderRadius: Math.min(14, borderRadius),
978
+ },
979
+ reanimated ? rewardAnimatedStyle : null,
980
+ scratchStyle,
981
+ ]}
982
+ >
983
+ {rewardIcon ? (
984
+ <View style={styles.rewardIcon}>{rewardIcon}</View>
985
+ ) : null}
986
+
987
+ <Text
988
+ style={[
989
+ styles.rewardText,
990
+ {
991
+ color: themeText,
992
+ },
993
+ rewardStyle,
994
+ ]}
995
+ >
996
+ {reward || "🎉 Reward Revealed!"}
997
+ </Text>
998
+ </Animated.View>
999
+ ) : (
1000
+ <ScratchSurface
1001
+ width={width - 44}
1002
+ height={Math.min(82, height * 0.38)}
1003
+ colors={builtInTheme.scratch}
1004
+ borderColor={builtInTheme.scratchBorder}
1005
+ scratchText={resolvedScratchText}
1006
+ iconColor={builtInTheme.scratchIcon}
1007
+ threshold={scratchThreshold}
1008
+ revealed={revealed || disabled}
1009
+ onProgress={handleProgress}
1010
+ onReveal={reveal}
1011
+ style={scratchStyle}
1012
+ />
1013
+ )}
1014
+ </View>
1015
+
1016
+ {/* =========================================
1017
+ OPTIONAL CUSTOM CHILDREN
1018
+ ========================================= */}
1019
+
1020
+ {children}
1021
+
1022
+ {/* =========================================
1023
+ OPTIONAL PRESS OVERLAY
1024
+ ========================================= */}
1025
+
1026
+ {onPress && !disabled ? (
1027
+ <View pointerEvents="box-none" style={styles.pressLayer}>
1028
+ {/* Intentionally empty.
1029
+ ScratchSurface receives touch priority. */}
1030
+ </View>
1031
+ ) : null}
1032
+ </View>
1033
+
1034
+ {/* Optional external press callback */}
1035
+ {onPress && !disabled ? <PressableProxy onPress={handlePress} /> : null}
1036
+ </View>
1037
+ );
1038
+ },
1039
+ );
1040
+
1041
+ /* =========================================================
1042
+ VISUAL ICON
1043
+ ========================================================= */
1044
+
1045
+ function ThemeVisualIcon({ theme }) {
1046
+ return getThemeIcon(theme, 68);
1047
+ }
1048
+
1049
+ /* =========================================================
1050
+ PRESSABLE PROXY
1051
+ ========================================================= */
1052
+
1053
+ /*
1054
+ * We don't put Pressable around the whole card because
1055
+ * that can interfere with the scratch PanResponder.
1056
+ *
1057
+ * onPress is therefore intended for programmatic/custom
1058
+ * handling while the scratch area owns the gesture.
1059
+ */
1060
+ function PressableProxy() {
1061
+ return null;
1062
+ }
1063
+
1064
+ /* =========================================================
1065
+ DISPLAY NAME
1066
+ ========================================================= */
1067
+
1068
+ UIScratchCard.displayName = "UIScratchCard";
1069
+
1070
+ /* =========================================================
1071
+ STYLES
1072
+ ========================================================= */
1073
+
1074
+ const styles = StyleSheet.create({
1075
+ wrapper: {
1076
+ alignSelf: "center",
1077
+ },
1078
+
1079
+ card: {
1080
+ position: "relative",
1081
+ overflow: "hidden",
1082
+ borderWidth: 1.2,
1083
+ },
1084
+
1085
+ content: {
1086
+ position: "absolute",
1087
+ top: 18,
1088
+ left: 20,
1089
+ right: 16,
1090
+ height: 105,
1091
+ flexDirection: "row",
1092
+ alignItems: "flex-start",
1093
+ },
1094
+
1095
+ textArea: {
1096
+ flex: 1,
1097
+ paddingRight: 4,
1098
+ },
1099
+
1100
+ title: {
1101
+ fontSize: 27,
1102
+ lineHeight: 29,
1103
+ fontWeight: "900",
1104
+ letterSpacing: -0.5,
1105
+ },
1106
+
1107
+ subtitle: {
1108
+ marginTop: 6,
1109
+ fontSize: 13,
1110
+ lineHeight: 17,
1111
+ fontWeight: "500",
1112
+ },
1113
+
1114
+ visualArea: {
1115
+ width: 100,
1116
+ height: 88,
1117
+ alignItems: "center",
1118
+ justifyContent: "center",
1119
+ },
1120
+
1121
+ visualImage: {
1122
+ width: 100,
1123
+ height: 88,
1124
+ },
1125
+
1126
+ decorations: {
1127
+ ...StyleSheet.absoluteFillObject,
1128
+ },
1129
+
1130
+ decorationStar: {
1131
+ position: "absolute",
1132
+ top: 14,
1133
+ right: 17,
1134
+ fontSize: 17,
1135
+ },
1136
+
1137
+ decorationStarTwo: {
1138
+ top: 46,
1139
+ right: 120,
1140
+ fontSize: 12,
1141
+ },
1142
+
1143
+ decorationStarThree: {
1144
+ top: 10,
1145
+ left: 15,
1146
+ fontSize: 12,
1147
+ },
1148
+
1149
+ decorationStarFour: {
1150
+ bottom: 90,
1151
+ right: 155,
1152
+ fontSize: 11,
1153
+ },
1154
+
1155
+ scratchWrapper: {
1156
+ position: "absolute",
1157
+ },
1158
+
1159
+ scratchSurface: {
1160
+ position: "relative",
1161
+ overflow: "hidden",
1162
+ borderWidth: 1.2,
1163
+ borderRadius: 14,
1164
+ },
1165
+
1166
+ scratchPrompt: {
1167
+ ...StyleSheet.absoluteFillObject,
1168
+ alignItems: "center",
1169
+ justifyContent: "center",
1170
+ },
1171
+
1172
+ scratchPromptText: {
1173
+ marginTop: 1,
1174
+ fontSize: 14,
1175
+ lineHeight: 18,
1176
+ fontWeight: "700",
1177
+ },
1178
+
1179
+ rewardContainer: {
1180
+ flex: 1,
1181
+ borderWidth: 1.2,
1182
+ backgroundColor: "#FFFFFF",
1183
+ alignItems: "center",
1184
+ justifyContent: "center",
1185
+ paddingHorizontal: 12,
1186
+ },
1187
+
1188
+ rewardIcon: {
1189
+ marginBottom: 4,
1190
+ alignItems: "center",
1191
+ justifyContent: "center",
1192
+ },
1193
+
1194
+ rewardText: {
1195
+ fontSize: 18,
1196
+ lineHeight: 23,
1197
+ fontWeight: "900",
1198
+ textAlign: "center",
1199
+ },
1200
+
1201
+ pressLayer: {
1202
+ ...StyleSheet.absoluteFillObject,
1203
+ },
1204
+ });
1205
+
1206
+ /* =========================================================
1207
+ EXPORTS
1208
+ ========================================================= */
1209
+
1210
+ export default UIScratchCard;
1211
+
1212
+ export {
1213
+ UIScratchCard,
1214
+ THEMES as UIScratchCardThemes,
1215
+ VARIANT_LABELS as UIScratchCardLabels,
1216
+ };
@@ -0,0 +1,5 @@
1
+ export {
2
+ default as UIScratchCard,
3
+ UIScratchCardThemes,
4
+ UIScratchCardLabels,
5
+ } from "./UIScratchCard";
@@ -125,3 +125,8 @@ export {
125
125
  export { default as UILayout } from "./layout/UILayout";
126
126
  export { default as UIHeader } from "./layout/UIHeader";
127
127
  export { default as UIBannerCarousel } from "./UIBannerCarousel";
128
+ export {
129
+ default as UIScratchCard,
130
+ UIScratchCardThemes,
131
+ UIScratchCardLabels,
132
+ } from "./UIScratchCard";
package/src/index.js CHANGED
@@ -135,4 +135,7 @@ export {
135
135
  UILayout,
136
136
  UIHeader,
137
137
  UIBannerCarousel,
138
+ UIScratchCard,
139
+ UIScratchCardThemes,
140
+ UIScratchCardLabels,
138
141
  } from "./components";