@@ -10,34 +10,46 @@ import {
1010 ActivityIndicator ,
1111 Modal ,
1212 FlatList ,
13+ Share ,
1314} from 'react-native' ;
1415import { colors , spacing , typography , borderRadius } from '../utils/constants' ;
1516import { useLoyaltyStore } from '../store/loyaltyStore' ;
1617import { useWalletStore } from '../store/walletStore' ;
18+ import { useGamificationStore } from '../store/gamificationStore' ;
1719import { Card } from '../components/common/Card' ;
18- import { LoyaltyTier , RewardType , TierBenefits } from '../types/loyalty' ;
20+ import { LoyaltyTier , RewardType , TierBenefits , PointTxType , StreakInfo } from '../types/loyalty' ;
1921
2022const 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