Skip to content

Commit 7e78c8e

Browse files
committed
feat(frontend): integrate loyalty store with contract, add streaks/badges/refs to screen
- New types: PointTxType, LoyaltyConfig, StreakInfo, ReferralInfo - loyaltyStore: contract integration stubs, streak tracking, referral bonuses, gamification triggers - LoyaltyDashboardScreen: streak card, referral share, badges modal, points expiry UI, tier comparison table - gamificationService: 8 new loyalty achievements and badges (point milestones, streak milestones, referral milestones) - Gamification triggers wired into accumulatePoints and earnReferralBonus flows
1 parent 8690689 commit 7e78c8e

5 files changed

Lines changed: 561 additions & 11 deletions

File tree

src/screens/LoyaltyDashboardScreen.tsx

Lines changed: 268 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,34 +10,46 @@ import {
1010
ActivityIndicator,
1111
Modal,
1212
FlatList,
13+
Share,
1314
} from 'react-native';
1415
import { colors, spacing, typography, borderRadius } from '../utils/constants';
1516
import { useLoyaltyStore } from '../store/loyaltyStore';
1617
import { useWalletStore } from '../store/walletStore';
18+
import { useGamificationStore } from '../store/gamificationStore';
1719
import { Card } from '../components/common/Card';
18-
import { LoyaltyTier, RewardType, TierBenefits } from '../types/loyalty';
20+
import { LoyaltyTier, RewardType, TierBenefits, PointTxType, StreakInfo } from '../types/loyalty';
1921

2022
const LoyaltyDashboardScreen: React.FC = () => {
2123
const {
2224
loyaltyStatus,
2325
transactions,
2426
rewards,
2527
program,
28+
streak,
29+
referral,
2630
isLoading,
2731
initializeProgram,
32+
fetchLoyaltyStatus,
2833
accumulatePoints,
2934
redeemPoints,
35+
earnReferralBonus,
36+
generateReferralCode,
3037
} = useLoyaltyStore();
3138
const { address } = useWalletStore();
39+
const { earnedBadges, earnedAchievements } = useGamificationStore();
3240

3341
const [modalVisible, setModalVisible] = useState(false);
3442
const [selectedReward, setSelectedReward] = useState<string>('');
43+
const [badgeModalVisible, setBadgeModalVisible] = useState(false);
3544

3645
useEffect(() => {
3746
if (!program) {
3847
initializeProgram();
3948
}
40-
}, [program, initializeProgram]);
49+
if (address) {
50+
fetchLoyaltyStatus(address);
51+
}
52+
}, [program, initializeProgram, address, fetchLoyaltyStatus]);
4153

4254
useEffect(() => {
4355
if (address && loyaltyStatus) {
@@ -48,6 +60,18 @@ const LoyaltyDashboardScreen: React.FC = () => {
4860
}
4961
}, [address, loyaltyStatus]);
5062

63+
const handleShareReferral = useCallback(async () => {
64+
const code = generateReferralCode();
65+
try {
66+
await Share.share({
67+
message: `Join SubTrackr and use my referral code: ${code}. You'll earn bonus points!`,
68+
title: 'Invite a Friend',
69+
});
70+
} catch {
71+
// user cancelled
72+
}
73+
}, [generateReferralCode]);
74+
5175
const handleRedeemReward = useCallback(async () => {
5276
if (!selectedReward) {
5377
Alert.alert('Error', 'Please select a reward');
@@ -84,6 +108,84 @@ const LoyaltyDashboardScreen: React.FC = () => {
84108
return program.tiers[currentTierIndex + 1];
85109
};
86110

111+
const renderStreakCard = () => {
112+
if (!loyaltyStatus) return null;
113+
const currentStreak = loyaltyStatus.streak || streak.current;
114+
return (
115+
<Card style={styles.streakCard}>
116+
<View style={styles.streakHeader}>
117+
<Text style={styles.streakIcon}>🔥</Text>
118+
<View style={styles.streakInfo}>
119+
<Text style={styles.streakValue}>
120+
{currentStreak > 0 ? `${currentStreak}-day streak` : 'Start a streak!'}
121+
</Text>
122+
<Text style={styles.streakSubtext}>
123+
{currentStreak >= 10
124+
? 'Amazing! You earned a streak bonus!'
125+
: currentStreak >= 5
126+
? 'Keep going! Almost at bonus milestone.'
127+
: 'Pay on time to build your streak.'}
128+
</Text>
129+
</View>
130+
</View>
131+
{currentStreak > 0 && (
132+
<View style={styles.streakProgress}>
133+
<View style={styles.streakBar}>
134+
<View
135+
style={[
136+
styles.streakFill,
137+
{ width: `${Math.min(100, (currentStreak % 10) * 10)}%` },
138+
]}
139+
/>
140+
</View>
141+
<Text style={styles.streakMilestone}>
142+
{10 - (currentStreak % 10)} charges to next streak bonus
143+
</Text>
144+
</View>
145+
)}
146+
</Card>
147+
);
148+
};
149+
150+
const renderReferralCard = () => (
151+
<Card style={styles.referralCard}>
152+
<Text style={styles.referralTitle}>Refer a Friend</Text>
153+
<Text style={styles.referralDesc}>
154+
Earn {referral.bonusPoints} bonus points for each friend who joins!
155+
</Text>
156+
<TouchableOpacity style={styles.shareButton} onPress={handleShareReferral}>
157+
<Text style={styles.shareButtonText}>Share Referral Code</Text>
158+
</TouchableOpacity>
159+
{referral.totalReferrals > 0 && (
160+
<Text style={styles.referralStats}>
161+
{referral.totalReferrals} friend{referral.totalReferrals > 1 ? 's' : ''} joined
162+
</Text>
163+
)}
164+
</Card>
165+
);
166+
167+
const renderBadgesCard = () => {
168+
if (earnedBadges.length === 0 && earnedAchievements.length === 0) return null;
169+
return (
170+
<Card style={styles.badgesCard}>
171+
<View style={styles.badgesHeader}>
172+
<Text style={styles.badgesTitle}>Badges & Achievements</Text>
173+
<TouchableOpacity onPress={() => setBadgeModalVisible(true)}>
174+
<Text style={styles.badgesViewAll}>View all →</Text>
175+
</TouchableOpacity>
176+
</View>
177+
<View style={styles.badgeRow}>
178+
{earnedBadges.slice(0, 4).map((badge, idx) => (
179+
<View key={idx} style={styles.badgeItem}>
180+
<Text style={styles.badgeIcon}>🏆</Text>
181+
<Text style={styles.badgeName} numberOfLines={1}>{badge}</Text>
182+
</View>
183+
))}
184+
</View>
185+
</Card>
186+
);
187+
};
188+
87189
const renderStatusCard = () => {
88190
if (!loyaltyStatus) {
89191
return (
@@ -204,10 +306,18 @@ const LoyaltyDashboardScreen: React.FC = () => {
204306
{transactions.length === 0 ? (
205307
<Text style={styles.emptyText}>No transactions yet</Text>
206308
) : (
207-
transactions.slice(0, 10).map((tx) => (
309+
transactions.slice(0, 15).map((tx) => (
208310
<View key={tx.id} style={styles.transactionItem}>
209311
<View style={styles.transactionInfo}>
210312
<Text style={styles.transactionDesc}>{tx.description}</Text>
313+
<Text style={styles.transactionType}>
314+
{tx.type === PointTxType.EARNED && 'Earned'}
315+
{tx.type === PointTxType.REDEEMED && 'Redeemed'}
316+
{tx.type === PointTxType.EXPIRED && 'Expired'}
317+
{tx.type === PointTxType.REFERRAL_BONUS && 'Referral'}
318+
{tx.type === PointTxType.STREAK_BONUS && 'Streak Bonus'}
319+
{tx.type === PointTxType.ACHIEVEMENT && 'Achievement'}
320+
</Text>
211321
<Text style={styles.transactionDate}>
212322
{new Date(tx.createdAt).toLocaleDateString()}
213323
</Text>
@@ -226,7 +336,7 @@ const LoyaltyDashboardScreen: React.FC = () => {
226336
</Card>
227337
);
228338

229-
const renderMembers = () => {
339+
const renderTierComparison = () => {
230340
if (!program) return null;
231341
return (
232342
<Card style={styles.membersCard}>
@@ -271,9 +381,12 @@ const LoyaltyDashboardScreen: React.FC = () => {
271381
</View>
272382

273383
{renderStatusCard()}
384+
{renderStreakCard()}
385+
{renderBadgesCard()}
386+
{renderReferralCard()}
274387
{renderRewardsCard()}
275388
{renderTransactionsCard()}
276-
{renderMembers()}
389+
{renderTierComparison()}
277390
</ScrollView>
278391

279392
<Modal
@@ -327,6 +440,36 @@ const LoyaltyDashboardScreen: React.FC = () => {
327440
</View>
328441
</View>
329442
</Modal>
443+
444+
<Modal
445+
visible={badgeModalVisible}
446+
animationType="slide"
447+
transparent={true}
448+
onRequestClose={() => setBadgeModalVisible(false)}>
449+
<View style={styles.modalOverlay}>
450+
<View style={styles.modalContent}>
451+
<Text style={styles.modalTitle}>Badges & Achievements</Text>
452+
<Text style={styles.modalSubtitle}>
453+
{earnedBadges.length} badges earned
454+
</Text>
455+
<FlatList
456+
data={earnedBadges}
457+
keyExtractor={(item, idx) => `${idx}`}
458+
renderItem={({ item: badge }) => (
459+
<View style={styles.badgeRow}>
460+
<Text style={styles.badgeIcon}>🏆</Text>
461+
<Text style={styles.badgeName}>{badge}</Text>
462+
</View>
463+
)}
464+
/>
465+
<TouchableOpacity
466+
style={styles.cancelButton}
467+
onPress={() => setBadgeModalVisible(false)}>
468+
<Text style={styles.cancelButtonText}>Close</Text>
469+
</TouchableOpacity>
470+
</View>
471+
</View>
472+
</Modal>
330473
</SafeAreaView>
331474
);
332475
};
@@ -587,6 +730,126 @@ const styles = StyleSheet.create({
587730
textAlign: 'center',
588731
marginTop: spacing.xs,
589732
},
733+
transactionType: {
734+
fontSize: typography.fontSizeXs,
735+
color: colors.primary,
736+
marginTop: spacing.xs,
737+
},
738+
streakCard: {
739+
padding: spacing.md,
740+
margin: spacing.md,
741+
marginTop: 0,
742+
},
743+
streakHeader: {
744+
flexDirection: 'row',
745+
alignItems: 'center',
746+
},
747+
streakIcon: {
748+
fontSize: 32,
749+
marginRight: spacing.md,
750+
},
751+
streakInfo: {
752+
flex: 1,
753+
},
754+
streakValue: {
755+
fontSize: typography.fontSizeMd,
756+
fontWeight: typography.fontWeightBold,
757+
color: colors.text,
758+
},
759+
streakSubtext: {
760+
fontSize: typography.fontSizeSm,
761+
color: colors.textSecondary,
762+
marginTop: spacing.xs,
763+
},
764+
streakProgress: {
765+
marginTop: spacing.md,
766+
},
767+
streakBar: {
768+
height: 6,
769+
backgroundColor: colors.border,
770+
borderRadius: 3,
771+
overflow: 'hidden',
772+
},
773+
streakFill: {
774+
height: '100%',
775+
backgroundColor: '#FF6B35',
776+
},
777+
streakMilestone: {
778+
fontSize: typography.fontSizeXs,
779+
color: colors.textSecondary,
780+
marginTop: spacing.xs,
781+
},
782+
referralCard: {
783+
padding: spacing.md,
784+
margin: spacing.md,
785+
marginTop: 0,
786+
},
787+
referralTitle: {
788+
fontSize: typography.fontSizeMd,
789+
fontWeight: typography.fontWeightBold,
790+
color: colors.text,
791+
marginBottom: spacing.xs,
792+
},
793+
referralDesc: {
794+
fontSize: typography.fontSizeSm,
795+
color: colors.textSecondary,
796+
marginBottom: spacing.md,
797+
},
798+
shareButton: {
799+
backgroundColor: colors.primary,
800+
borderRadius: borderRadius.md,
801+
padding: spacing.md,
802+
alignItems: 'center',
803+
},
804+
shareButtonText: {
805+
color: colors.text,
806+
fontSize: typography.fontSizeMd,
807+
fontWeight: typography.fontWeightBold,
808+
},
809+
referralStats: {
810+
fontSize: typography.fontSizeSm,
811+
color: colors.success,
812+
marginTop: spacing.sm,
813+
textAlign: 'center',
814+
},
815+
badgesCard: {
816+
padding: spacing.md,
817+
margin: spacing.md,
818+
marginTop: 0,
819+
},
820+
badgesHeader: {
821+
flexDirection: 'row',
822+
justifyContent: 'space-between',
823+
alignItems: 'center',
824+
marginBottom: spacing.md,
825+
},
826+
badgesTitle: {
827+
fontSize: typography.fontSizeMd,
828+
fontWeight: typography.fontWeightBold,
829+
color: colors.text,
830+
},
831+
badgesViewAll: {
832+
fontSize: typography.fontSizeSm,
833+
color: colors.primary,
834+
},
835+
badgeRow: {
836+
flexDirection: 'row',
837+
flexWrap: 'wrap',
838+
gap: spacing.sm,
839+
},
840+
badgeItem: {
841+
alignItems: 'center',
842+
width: 60,
843+
},
844+
badgeIcon: {
845+
fontSize: 28,
846+
},
847+
badgeName: {
848+
fontSize: typography.fontSizeXs,
849+
color: colors.textSecondary,
850+
marginTop: spacing.xs,
851+
textAlign: 'center',
852+
},
590853
modalOverlay: {
591854
flex: 1,
592855
backgroundColor: 'rgba(0, 0, 0, 0.5)',

0 commit comments

Comments
 (0)