Modernize confetti
This commit is contained in:
5 files changed
+179
-53
No files matched your search
@@ -81,7 +81,6 @@ history, code and binary blobs related to React Native functionality filtered ou
|
||||
- replace [moment](https://momentjs.com/docs/#/-project-status/) (maintenance mode) with
|
||||
`date-fns`
|
||||
- replace unmaintained UI packages:
|
||||
- `react-native-confetti` (last release 2017, mocked out in tests)
|
||||
- `react-native-simple-radio-button` (react-native-ui-lib already ships RadioButton/RadioGroup)
|
||||
- migrate ESLint 8 + airbnb config (both EOL) to ESLint 9 flat config or
|
||||
[eslint-config-expo](https://docs.expo.dev/guides/using-eslint/)
|
||||
|
||||
+23
-10
@@ -37,16 +37,29 @@ jest.mock('expo-notifications', () => ({
|
||||
scheduleNotificationAsync: jest.fn(async () => 'notification-id'),
|
||||
}));
|
||||
|
||||
// Visual only, and old enough that it fights with the test renderer.
|
||||
jest.mock('react-native-confetti', () => {
|
||||
// Visual only, and Skia's native canvas isn't available in Jest.
|
||||
jest.mock('react-native-fast-confetti', () => {
|
||||
const React = require('react');
|
||||
return class Confetti extends React.Component {
|
||||
startConfetti() {}
|
||||
|
||||
stopConfetti() {}
|
||||
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
const Confetti = React.forwardRef(function Confetti(props, ref) {
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
restart() {},
|
||||
pause() {},
|
||||
resume() {},
|
||||
reset() {},
|
||||
}));
|
||||
return null;
|
||||
});
|
||||
const PIConfetti = React.forwardRef(function PIConfetti(props, ref) {
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
restart() {},
|
||||
pause() {},
|
||||
resume() {},
|
||||
reset() {},
|
||||
}));
|
||||
return null;
|
||||
});
|
||||
PIConfetti.Origin = function Origin() {
|
||||
return null;
|
||||
};
|
||||
return { Confetti, PIConfetti };
|
||||
});
|
||||
Generated
+81
-17
@@ -15,6 +15,7 @@
|
||||
"@react-navigation/native": "^7.1.17",
|
||||
"@react-navigation/native-stack": "^7.3.26",
|
||||
"@sentry/react-native": "~7.11.0",
|
||||
"@shopify/react-native-skia": "2.4.18",
|
||||
"@stripe/stripe-react-native": "0.63.0",
|
||||
"email-validator": "^2.0.4",
|
||||
"expo": "^55.0.0",
|
||||
@@ -29,7 +30,7 @@
|
||||
"react": "19.2.0",
|
||||
"react-hook-form": "^7.52.1",
|
||||
"react-native": "0.83.6",
|
||||
"react-native-confetti": "^0.1.0",
|
||||
"react-native-fast-confetti": "^2.0.2",
|
||||
"react-native-gesture-handler": "~2.30.0",
|
||||
"react-native-reanimated": "4.2.1",
|
||||
"react-native-safe-area-context": "~5.6.0",
|
||||
@@ -49,7 +50,6 @@
|
||||
"@testing-library/react-native": "^13.3.3",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/react": "~19.2.10",
|
||||
"@types/react-native-confetti": "^0.1.4",
|
||||
"@types/react-native-simple-radio-button": "^2.7.7",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
@@ -3776,6 +3776,33 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@shopify/react-native-skia": {
|
||||
"version": "2.4.18",
|
||||
"resolved": "https://registry.npmjs.org/@shopify/react-native-skia/-/react-native-skia-2.4.18.tgz",
|
||||
"integrity": "sha512-/AB/mvb2dGSQVIJTxyG9ZMFn2PjwWlVcFxHU4TV+K25LCU49ntJXl4xda2ghXxdENMggKgO9R5pA+P/AQ0UUrA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"canvaskit-wasm": "0.40.0",
|
||||
"react-reconciler": "0.31.0"
|
||||
},
|
||||
"bin": {
|
||||
"setup-skia-web": "scripts/setup-canvaskit.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=19.0",
|
||||
"react-native": ">=0.78",
|
||||
"react-native-reanimated": ">=3.19.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-native": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native-reanimated": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@sinclair/typebox": {
|
||||
"version": "0.27.12",
|
||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.12.tgz",
|
||||
@@ -4297,16 +4324,6 @@
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-native-confetti": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-native-confetti/-/react-native-confetti-0.1.4.tgz",
|
||||
"integrity": "sha512-StgW1o0dw8ZLCVZ7v6ZVOBu+RYT+PFjtQAhF9k7uLn/avor4UtKhB9cDrF377+JfRw0aHeo3gg9dawZNzM/jOg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-native-simple-radio-button": {
|
||||
"version": "2.7.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-native-simple-radio-button/-/react-native-simple-radio-button-2.7.7.tgz",
|
||||
@@ -4927,6 +4944,12 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@webgpu/types": {
|
||||
"version": "0.1.21",
|
||||
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.21.tgz",
|
||||
"integrity": "sha512-pUrWq3V5PiSGFLeLxoGqReTZmiiXwY3jRkIG5sLLKjyqNxrwm/04b4nw7LSmGWJcKk59XOM/YRTUwOzo4MMlow==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@wry/caches": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@wry/caches/-/caches-1.0.1.tgz",
|
||||
@@ -5918,6 +5941,15 @@
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/canvaskit-wasm": {
|
||||
"version": "0.40.0",
|
||||
"resolved": "https://registry.npmjs.org/canvaskit-wasm/-/canvaskit-wasm-0.40.0.tgz",
|
||||
"integrity": "sha512-Od2o+ZmoEw9PBdN/yCGvzfu0WVqlufBPEWNG452wY7E9aT8RBE+ChpZF526doOlg7zumO4iCS+RAeht4P0Gbpw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@webgpu/types": "0.1.21"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
@@ -14291,11 +14323,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-confetti": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-confetti/-/react-native-confetti-0.1.0.tgz",
|
||||
"integrity": "sha512-+cXJvnPXKc0i/eTQcuzcjrlfIxmn/xFw4UyuVKQOWNK6BfWABvO6Ms6Zp6ivF1E8Vxt2vJkzJlNyoBvMh9FxKA==",
|
||||
"license": "MIT"
|
||||
"node_modules/react-native-fast-confetti": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-native-fast-confetti/-/react-native-fast-confetti-2.0.2.tgz",
|
||||
"integrity": "sha512-g/so5kYCQFMFNuD3ui10hMCXZTA8pPzOhwUJgzfslxI2WHuYb4SjdOxV0I9xPv7ts8Sqch6bS7oCJZMX2DM0Yw==",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"example",
|
||||
"kitchen-sink"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"@shopify/react-native-skia": ">=2.0.0 <3",
|
||||
"react": "*",
|
||||
"react-native": "*",
|
||||
"react-native-reanimated": ">=4.1.0 <5",
|
||||
"react-native-worklets": ">=0.7.0 <1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-gesture-handler": {
|
||||
"version": "2.30.1",
|
||||
@@ -14596,6 +14639,27 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-reconciler": {
|
||||
"version": "0.31.0",
|
||||
"resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.31.0.tgz",
|
||||
"integrity": "sha512-7Ob7Z+URmesIsIVRjnLoDGwBEG/tVitidU0nMsqX/eeJaLY89RISO/10ERe0MqmzuKUUB1rmY+h1itMbUHg9BQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"scheduler": "^0.25.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-reconciler/node_modules/scheduler": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
|
||||
"integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-redux": {
|
||||
"version": "9.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz",
|
||||
|
||||
+2
-2
@@ -22,6 +22,7 @@
|
||||
"@react-navigation/native": "^7.1.17",
|
||||
"@react-navigation/native-stack": "^7.3.26",
|
||||
"@sentry/react-native": "~7.11.0",
|
||||
"@shopify/react-native-skia": "2.4.18",
|
||||
"@stripe/stripe-react-native": "0.63.0",
|
||||
"email-validator": "^2.0.4",
|
||||
"expo": "^55.0.0",
|
||||
@@ -36,7 +37,7 @@
|
||||
"react": "19.2.0",
|
||||
"react-hook-form": "^7.52.1",
|
||||
"react-native": "0.83.6",
|
||||
"react-native-confetti": "^0.1.0",
|
||||
"react-native-fast-confetti": "^2.0.2",
|
||||
"react-native-gesture-handler": "~2.30.0",
|
||||
"react-native-reanimated": "4.2.1",
|
||||
"react-native-safe-area-context": "~5.6.0",
|
||||
@@ -56,7 +57,6 @@
|
||||
"@testing-library/react-native": "^13.3.3",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/react": "~19.2.10",
|
||||
"@types/react-native-confetti": "^0.1.4",
|
||||
"@types/react-native-simple-radio-button": "^2.7.7",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as Notifications from 'expo-notifications';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
Image,
|
||||
ScrollView,
|
||||
@@ -13,7 +13,9 @@ import {
|
||||
GestureResponderEvent,
|
||||
Switch,
|
||||
} from 'react-native';
|
||||
import Confetti from 'react-native-confetti';
|
||||
import { Confetti, PIConfetti } from 'react-native-fast-confetti';
|
||||
import type { ConfettiMethods, PIConfettiMethods } from 'react-native-fast-confetti';
|
||||
import { Easing } from 'react-native-reanimated';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import RadioButton from '@/components/RadioButton';
|
||||
@@ -130,14 +132,42 @@ const PurchaseButton = ({
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const UserConfetti = ({ user, confettiRef }: { user: UserType; confettiRef: any }) => {
|
||||
const UserConfetti = ({
|
||||
user,
|
||||
confettiRef,
|
||||
burstRef,
|
||||
containerRef,
|
||||
onRainStart,
|
||||
onRainEnd,
|
||||
}: {
|
||||
user: UserType;
|
||||
confettiRef: React.RefObject<ConfettiMethods | null>;
|
||||
burstRef: React.RefObject<PIConfettiMethods | null>;
|
||||
containerRef: React.RefObject<React.ComponentRef<typeof View> | null>;
|
||||
onRainStart: () => void;
|
||||
onRainEnd: () => void;
|
||||
}) => {
|
||||
if (!user.is_member) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Confetti!!!!
|
||||
return <Confetti ref={confettiRef} confettiCount={Number.MAX_SAFE_INTEGER} />;
|
||||
return (
|
||||
<View ref={containerRef} pointerEvents="none" style={styles.confetti}>
|
||||
<Confetti
|
||||
ref={confettiRef}
|
||||
autoplay={false}
|
||||
count={400}
|
||||
gravity={0.24}
|
||||
easing={Easing.linear}
|
||||
onAnimationStart={onRainStart}
|
||||
onAnimationEnd={onRainEnd}
|
||||
/>
|
||||
<PIConfetti ref={burstRef} autoplay={false} fadeOutOnEnd>
|
||||
<PIConfetti.Origin blastPosition="center" count={80} initialSpeed={2} />
|
||||
</PIConfetti>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
const Logo = ({ user }: { user: UserType }) => {
|
||||
if (!user.is_member) {
|
||||
@@ -216,8 +246,10 @@ interface ProofProps {
|
||||
onDeletePress: () => void;
|
||||
}
|
||||
const Proof = ({ onLogoutPress, fetchUser, isFetchingUserData, onDeletePress }: ProofProps) => {
|
||||
const CONFETTI_TIMEOUT = 15000;
|
||||
const confettiRef = useRef<Confetti>(null);
|
||||
const confettiRef = useRef<ConfettiMethods>(null);
|
||||
const burstRef = useRef<PIConfettiMethods>(null);
|
||||
const confettiContainerRef = useRef<React.ComponentRef<typeof View>>(null);
|
||||
const isRaining = useRef(false);
|
||||
const {
|
||||
paymentLoading,
|
||||
isLoadingMembershipType,
|
||||
@@ -231,22 +263,25 @@ const Proof = ({ onLogoutPress, fetchUser, isFetchingUserData, onDeletePress }:
|
||||
fetchUser,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const currentRef = confettiRef.current;
|
||||
return () => {
|
||||
if (currentRef) {
|
||||
currentRef.stopConfetti();
|
||||
}
|
||||
};
|
||||
});
|
||||
const onRainStart = useCallback(() => {
|
||||
isRaining.current = true;
|
||||
}, []);
|
||||
const onRainEnd = useCallback(() => {
|
||||
isRaining.current = false;
|
||||
}, []);
|
||||
|
||||
const onMembershipPress = () => {
|
||||
confettiRef.current?.startConfetti();
|
||||
setTimeout(() => {
|
||||
if (confettiRef && confettiRef.current) {
|
||||
confettiRef.current.stopConfetti();
|
||||
}
|
||||
}, CONFETTI_TIMEOUT);
|
||||
const onMembershipPress = (event: GestureResponderEvent) => {
|
||||
const { pageX, pageY } = event.nativeEvent;
|
||||
// measure() reports pageX/pageY in the same coordinate space as the touch,
|
||||
// which measureInWindow does not on Android.
|
||||
confettiContainerRef.current?.measure((x, y, width, height, originX, originY) => {
|
||||
burstRef.current?.restart({
|
||||
origins: [{ blastPosition: { x: pageX - originX, y: pageY - originY } }],
|
||||
});
|
||||
});
|
||||
if (!isRaining.current) {
|
||||
confettiRef.current?.restart();
|
||||
}
|
||||
};
|
||||
|
||||
if (!user) {
|
||||
@@ -259,7 +294,14 @@ const Proof = ({ onLogoutPress, fetchUser, isFetchingUserData, onDeletePress }:
|
||||
|
||||
return (
|
||||
<>
|
||||
<UserConfetti user={user} confettiRef={confettiRef} />
|
||||
<UserConfetti
|
||||
user={user}
|
||||
confettiRef={confettiRef}
|
||||
burstRef={burstRef}
|
||||
containerRef={confettiContainerRef}
|
||||
onRainStart={onRainStart}
|
||||
onRainEnd={onRainEnd}
|
||||
/>
|
||||
<View style={styles.container}>
|
||||
<ScrollView refreshControl={<RefreshControl refreshing={isFetchingUserData} onRefresh={fetchUser} />}>
|
||||
<View style={[theme.card, { paddingVertical: 16, backgroundColor: 'transparent' }]}>
|
||||
@@ -302,6 +344,14 @@ const Proof = ({ onLogoutPress, fetchUser, isFetchingUserData, onDeletePress }:
|
||||
export default Proof;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
confetti: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
zIndex: 1,
|
||||
},
|
||||
membershipStatus: {
|
||||
marginVertical: 16,
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user