diff --git a/apps/wallet/assets/img/misc/walless-cover.png b/apps/wallet/assets/img/misc/walless-cover.png new file mode 100644 index 000000000..ce724f69f Binary files /dev/null and b/apps/wallet/assets/img/misc/walless-cover.png differ diff --git a/apps/wallet/package.json b/apps/wallet/package.json index dc4eea052..70cb4d9fa 100644 --- a/apps/wallet/package.json +++ b/apps/wallet/package.json @@ -49,6 +49,7 @@ "@react-navigation/stack": "^6.3.16", "@solana/spl-token": "0.3.7", "@solana/web3.js": "1.92.3", + "@tanstack/react-query": "^5.51.21", "@taquito/signer": "^17.5.2", "@taquito/taquito": "^17.5.2", "@taquito/tzip12": "^16.2.0", @@ -111,6 +112,7 @@ "use-debounce": "^10.0.0" }, "devDependencies": { + "@tanstack/eslint-plugin-query": "^5.51.15", "@types/color": "^3.0.6", "@types/jest": "^29.2.1", "@types/pouchdb-adapter-memory": "^6.1.6", diff --git a/apps/wallet/src/components/StreakIndicator.tsx b/apps/wallet/src/components/StreakIndicator.tsx new file mode 100644 index 000000000..e5fa4c212 --- /dev/null +++ b/apps/wallet/src/components/StreakIndicator.tsx @@ -0,0 +1,48 @@ +import type { FC } from 'react'; +import { StyleSheet } from 'react-native'; +import { View } from 'react-native'; + +interface Props { + currentStreak: number; + streak: number; +} + +const StreakIndicator: FC = ({ currentStreak, streak }) => { + return ( + + {Array.from({ length: streak }, (_, i) => i + 1).map((streakIndex) => ( + + ))} + + ); +}; + +export default StreakIndicator; + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, + }, + streakCircle: { + width: 10, + height: 10, + borderRadius: 5, + }, + inactiveCircle: { + borderWidth: 1, + borderColor: '#798896', + }, + activeCircle: { + backgroundColor: '#17A3E1', + }, +}); diff --git a/apps/wallet/src/features/Explorer/LoyaltyBar.tsx b/apps/wallet/src/features/Explorer/LoyaltyBar.tsx index 597e90687..1db333357 100644 --- a/apps/wallet/src/features/Explorer/LoyaltyBar.tsx +++ b/apps/wallet/src/features/Explorer/LoyaltyBar.tsx @@ -16,6 +16,9 @@ const LoyaltyBar: FC = ({ style }) => { screen: 'Explore', params: { screen: 'Loyalty', + params: { + screen: 'Default', + }, }, }); }; diff --git a/apps/wallet/src/features/Explorer/index.tsx b/apps/wallet/src/features/Explorer/index.tsx index 60747d9d3..a14bc39e8 100644 --- a/apps/wallet/src/features/Explorer/index.tsx +++ b/apps/wallet/src/features/Explorer/index.tsx @@ -9,7 +9,6 @@ import { filterMap } from 'utils/widget'; import Header from './Header'; import Highlights from './Highlights'; -import LoyaltyBar from './LoyaltyBar'; import Missions from './Missions'; import Widgets from './Widgets'; @@ -42,7 +41,6 @@ export const ExplorerFeature: FC = ({ style }) => {
- diff --git a/apps/wallet/src/features/Explorer/internal.ts b/apps/wallet/src/features/Explorer/internal.ts index 3b61b4b76..9ab53dd17 100644 --- a/apps/wallet/src/features/Explorer/internal.ts +++ b/apps/wallet/src/features/Explorer/internal.ts @@ -1,19 +1,22 @@ import { navigate } from 'utils/navigation'; export const missions = [ - { - title: 'Earn Walless Points', - point: 100, - buttonText: 'Earn', - onPress: () => { - navigate('Dashboard', { - screen: 'Explore', - params: { - screen: 'Loyalty', - }, - }); - }, - }, + // { + // title: 'Earn Walless Points', + // point: 100, + // buttonText: 'Earn', + // onPress: () => { + // navigate('Dashboard', { + // screen: 'Explore', + // params: { + // screen: 'Loyalty', + // params: { + // screen: 'Default', + // }, + // }, + // }); + // }, + // }, { title: 'Refer your friends', point: 100, diff --git a/apps/wallet/src/features/Loyalty/ActionCard/CompletedTag.tsx b/apps/wallet/src/features/Loyalty/ActionCard/CompletedTag.tsx deleted file mode 100644 index 9f781ab09..000000000 --- a/apps/wallet/src/features/Loyalty/ActionCard/CompletedTag.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import type { FC } from 'react'; -import type { ViewStyle } from 'react-native'; -import { StyleSheet, View } from 'react-native'; -import { Text } from '@walless/gui'; -import { Check } from '@walless/icons'; - -import { sharedStyles } from '../internal'; - -interface Props { - style?: ViewStyle; -} - -const CompletedTag: FC = ({ style }) => { - return ( - - - - - Completed - - ); -}; - -export default CompletedTag; - -const styles = StyleSheet.create({ - container: { - backgroundColor: 'rgba(47, 200, 121, 0.15)', - }, - completedIcon: { - borderWidth: 1, - borderColor: 'rgba(47, 200, 121, 0.3)', - }, - text: { - fontSize: 10, - color: 'white', - }, -}); diff --git a/apps/wallet/src/features/Loyalty/ActionCard/CountDown.tsx b/apps/wallet/src/features/Loyalty/ActionCard/CountDown.tsx deleted file mode 100644 index 29e8275ad..000000000 --- a/apps/wallet/src/features/Loyalty/ActionCard/CountDown.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import type { FC } from 'react'; -import { useEffect, useState } from 'react'; -import { StyleSheet } from 'react-native'; -import { Text } from '@walless/gui'; - -import { formatCountdownTime } from '../internal'; - -interface Props { - initialTimeRemaining: number; -} - -const CountDown: FC = ({ initialTimeRemaining }) => { - const [timeRemaining, setTimeRemaining] = - useState(initialTimeRemaining); - - useEffect(() => { - const interval = setInterval(() => { - if (timeRemaining > 0) { - setTimeRemaining((prev) => prev - 1000); - } - }, 1000); - - return () => clearInterval(interval); - }, [initialTimeRemaining]); - - return ( - {formatCountdownTime(timeRemaining)} - ); -}; - -const styles = StyleSheet.create({ - counterText: { - fontSize: 11, - fontWeight: '500', - color: '#00B1FF', - }, -}); - -export default CountDown; diff --git a/apps/wallet/src/features/Loyalty/ActionCard/StreakBar.tsx b/apps/wallet/src/features/Loyalty/ActionCard/StreakBar.tsx deleted file mode 100644 index 012f26288..000000000 --- a/apps/wallet/src/features/Loyalty/ActionCard/StreakBar.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import type { FC } from 'react'; -import type { ViewStyle } from 'react-native'; -import { StyleSheet, View } from 'react-native'; -import { Text } from '@walless/gui'; - -interface Props { - style?: ViewStyle; - currentStreak: number; - streak: number; - isRecorded: boolean; -} - -const StreakBar: FC = ({ style, currentStreak, streak, isRecorded }) => { - return ( - - - {Array.from({ length: streak }, (_, i) => ( - - ))} - - - - {isRecorded ? 'Recorded streak' : ''} - {`${currentStreak}/${streak}`} - - - ); -}; - -const styles = StyleSheet.create({ - container: { - gap: 4, - }, - streakContainer: { - flexDirection: 'row', - gap: 4, - }, - bar: { - height: 3, - borderRadius: 4, - flex: 1, - backgroundColor: '#515151', - }, - activeBar: { - backgroundColor: '#00B1FF', - }, - textContainer: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - }, - text: { - fontSize: 10, - color: '#A4B3C1', - }, -}); - -export default StreakBar; diff --git a/apps/wallet/src/features/Loyalty/ActionCard/VerificationNeededTag.tsx b/apps/wallet/src/features/Loyalty/ActionCard/VerificationNeededTag.tsx deleted file mode 100644 index 7c585db19..000000000 --- a/apps/wallet/src/features/Loyalty/ActionCard/VerificationNeededTag.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import type { FC } from 'react'; -import type { ViewStyle } from 'react-native'; -import { StyleSheet, View } from 'react-native'; -import { Text } from '@walless/gui'; -import { Exclamation2 } from '@walless/icons'; - -import { sharedStyles } from '../internal'; - -interface Props { - style?: ViewStyle; -} - -const VerificationNeededTag: FC = ({ style }) => { - return ( - - - - - Verification Needed - - ); -}; - -export default VerificationNeededTag; - -const styles = StyleSheet.create({ - container: { - backgroundColor: 'rgba(255, 190, 102, 0.15)', - }, - verificationIcon: { - borderWidth: 1, - borderColor: 'rgba(255, 190, 102, 0.3)', - }, - text: { - fontSize: 10, - color: '#FFBE66', - }, -}); diff --git a/apps/wallet/src/features/Loyalty/ActionCard/index.tsx b/apps/wallet/src/features/Loyalty/ActionCard/index.tsx deleted file mode 100644 index 268348d3b..000000000 --- a/apps/wallet/src/features/Loyalty/ActionCard/index.tsx +++ /dev/null @@ -1,349 +0,0 @@ -import type { FC } from 'react'; -import { useMemo, useState } from 'react'; -import type { ViewStyle } from 'react-native'; -import { - ActivityIndicator, - Image, - Linking, - StyleSheet, - Text, - View, -} from 'react-native'; -import { logger } from '@walless/core'; -import type { - Action, - ActionCount, - ActionMetadata, - ActionRecord, - UserProgress, -} from '@walless/graphql'; -import { - ActionCategory, - mutations, - queries, - VerifyMechanism, -} from '@walless/graphql'; -import { Button } from '@walless/gui'; -import { showNotificationModal } from 'modals/Notification'; -import { loyaltyActions, loyaltyState } from 'state/loyalty'; -import { qlClient } from 'utils/graphql'; -import { useSnapshot } from 'utils/hooks'; - -import PointTag from '../components/PointTag'; -import { - checkIfUserCanDoAction, - extractDataFromMetadata, - getActionLogo, - getCycleEndTime, - navigateInternalByCta, - sharedStyles, -} from '../internal'; - -import CompletedTag from './CompletedTag'; -import CountDown from './CountDown'; -import StreakBar from './StreakBar'; -import VerificationNeededTag from './VerificationNeededTag'; - -interface Props { - style?: ViewStyle; - action: Action; -} - -const ActionCard: FC = ({ style, action }) => { - const { userProgress, typeActionMap } = useSnapshot(loyaltyState); - const { name, desc, icon, ctaText, ctaType, cta } = useMemo(() => { - return extractDataFromMetadata(action.metadata as ActionMetadata[]); - }, [action]); - - const [isDoingAction, setIsDoingAction] = useState(false); - - const canUserDoAction = checkIfUserCanDoAction( - userProgress as UserProgress, - action, - ); - - const initialTimeRemaining = useMemo(() => { - if ( - !userProgress || - canUserDoAction || - action.category !== ActionCategory.Recurring || - !action.cycleInHours - ) { - return null; - } - - const lastRecord = (userProgress.actionRecords as ActionRecord[]).findLast( - (record) => record.actionId === action.id, - ); - if (!lastRecord) { - return 0; - } - - const cycleEndTime = getCycleEndTime( - new Date(lastRecord.timestamp), - action.cycleInHours as number, - ); - - return cycleEndTime.getTime() - Date.now(); - }, [userProgress]); - - const isRecorded = useMemo(() => { - if (!userProgress?.actionRecords) return false; - - const relatedRecurringAction = typeActionMap - .get(action.type!) - ?.find((a) => a.category === ActionCategory.Recurring); - - if (relatedRecurringAction?.cycleInHours) { - const lastRecord = userProgress.actionRecords.findLast( - (record) => record!.actionId === relatedRecurringAction.id, - ); - if (!lastRecord) { - return false; - } - - const cycleEndTime = getCycleEndTime( - new Date(lastRecord.timestamp), - relatedRecurringAction.cycleInHours, - ); - - return new Date() < cycleEndTime; - } - - return false; - }, [action, userProgress, typeActionMap]); - - const handleDoAction = async () => { - if (ctaType === 'internal') { - navigateInternalByCta(cta); - return; - } - - if (ctaType === '' || action.mechanism === VerifyMechanism.No) { - setIsDoingAction(true); - - try { - if (action.category === ActionCategory.Onetime) { - await qlClient.request(mutations.doLoyaltyAction, { - actionId: action.id, - }); - } else if (action.category === ActionCategory.Recurring) { - await qlClient.request( - mutations.doRecurringThenStreakThenMilestoneActionsByType, - { - type: action.type, - }, - ); - } - - const { loyaltyUserProgress } = await qlClient.request<{ - loyaltyUserProgress: UserProgress; - }>(queries.loyaltyUserProgress); - - loyaltyActions.setUserProgress(loyaltyUserProgress); - } catch (error) { - logger.error(error); - - showNotificationModal({ - id: `action-error-${action.id}`, - message: `Failed to perform action ${name}`, - textStyle: { - color: 'red', - }, - timeout: 5000, - }); - } - - setIsDoingAction(false); - } - - if (ctaType === 'external') { - Linking.openURL(cta); - } - }; - - const stat = useMemo(() => { - if (!userProgress) { - return null; - } - - return (userProgress.trackList as ActionCount[]).find( - (track) => track.type === action.type, - ); - }, [userProgress]); - - const currentStreak = useMemo(() => { - if (!stat || !stat.streaks) { - return 0; - } - - const cycleEndTime = getCycleEndTime( - new Date(stat.lastClaim), - stat.cycleInHours as number, - ); - - cycleEndTime.setHours( - cycleEndTime.getHours() + (stat.cycleInHours as number), - ); - - if (new Date() > cycleEndTime) { - return 0; - } - - let currentStreak = 0; - for (const streak of stat.streaks) { - if (streak?.streak === action.streak) { - currentStreak = streak?.currentStreak ?? 0; - } - } - return currentStreak; - }, [stat, action.streak]); - - const isPassthrough = - !canUserDoAction && action.category !== ActionCategory.Streak; - - return ( - - - - - {icon ? ( - - ) : ( - getActionLogo(action) - )} - - - - {name} - {desc !== '' && ( - - {desc} - - )} - - - - - - - {!isPassthrough && action.mechanism === VerifyMechanism.Manual && ( - - )} - - {isPassthrough && } - - {initialTimeRemaining && ( - - - - )} - - {action.category === ActionCategory.Streak && action.streak && ( - - )} - - - - {canUserDoAction && - action.category !== ActionCategory.Milestone && - action.category !== ActionCategory.Streak && - (isDoingAction ? ( - - ) : ( -