diff --git a/.actrc b/.actrc new file mode 100644 index 00000000000..e69de29bb2d diff --git a/.github/workflows/on_push_pr_preview.yml b/.github/workflows/on_push_pr_preview.yml new file mode 100644 index 00000000000..acfa0d29e0a --- /dev/null +++ b/.github/workflows/on_push_pr_preview.yml @@ -0,0 +1,20 @@ +name: "1 [on_push] Deploy PR version to Firebase for" + +on: + push: + branches: + - ops/prs-preview + +jobs: + deploy_on_firebase: + name: Deploy PR version to Firebase + uses: ./.github/workflows/on_workflow_pr_preview.yml + with: + ENV: "testing" + PUSH_RELEASE_TO_SENTRY: false + CHANNEL: "preview" + EXPIRES: "2d" + REF: "refs/heads/ops/prs-preview" + CACHE_BUCKET_NAME: "passculture-metier-ehp" + GCP_EHP_SERVICE_ACCOUNT: ${{ secrets.GCP_EHP_SERVICE_ACCOUNT }} + GCP_EHP_WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.GCP_EHP_WORKLOAD_IDENTITY_PROVIDER }} diff --git a/.github/workflows/on_workflow_pr_preview.yml b/.github/workflows/on_workflow_pr_preview.yml new file mode 100644 index 00000000000..65ca5575df0 --- /dev/null +++ b/.github/workflows/on_workflow_pr_preview.yml @@ -0,0 +1,107 @@ +name: "2 [on_workflow/PR] Deploy PR version for validation" + +on: + workflow_call: + inputs: + ENV: + type: string + required: true + PUSH_RELEASE_TO_SENTRY: + description: "If true, creates a release in Sentry and uploads sourcemaps" + type: boolean + default: false + CHANNEL: + type: string + required: true + EXPIRES: + type: string + default: "2d" + REF: + type: string + required: true + CACHE_BUCKET_NAME: + type: string + required: true + secrets: + GCP_EHP_SERVICE_ACCOUNT: + required: true + GCP_EHP_WORKLOAD_IDENTITY_PROVIDER: + required: true + +defaults: + run: + working-directory: '.' + +jobs: + deploy_on_firebase: + runs-on: ubuntu-22.04 + if: ${{ github.actor != 'dependabot[bot]' }} + steps: + - uses: actions/checkout@v4.2.1 + with: + ref: ${{ inputs.REF }} + - uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + - name: "OpenID Connect Authentication" + if: ${{ !github.event.act }} + id: "openid-auth" + uses: "google-github-actions/auth@v2" + with: + workload_identity_provider: ${{ secrets.GCP_EHP_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ secrets.GCP_EHP_SERVICE_ACCOUNT }} + - name: "Get Secret" + # if: ${{ !github.event.act }} + id: "secrets" + uses: "google-github-actions/get-secretmanager-secrets@v2" + with: + secrets: |- + FIREBASE_TOKEN:passculture-metier-ehp/pc_native_${{ inputs.ENV }}_firebase_json + # ENTRY_TOKEN:passculture-metier-ehp/passculture-app-native-sentry-token + - name: "Cache the node_modules" + if: ${{ !github.event.act }} + id: "yarn-modules-cache" + uses: pass-culture-github-actions/cache@v1.0.0 + with: + compression-method: "gzip" + bucket: ${{ inputs.CACHE_BUCKET_NAME }} + path: | + **/node_modules + key: v1-yarn-pro-dependency-cache-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + v1-yarn-pro-dependency-cache-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} + - run: yarn install --immutable + - run: yarn build:${{ inputs.ENV }} + env: + # By default NodeJS processes are limited to 512MB of memory + # This is not enough for the build process when compiling sourcemaps + # Increases the limit so that the build doesnt fail + NODE_OPTIONS: --max_old_space_size=4096 + - if: inputs.ENV != 'testing' + run: | + cat package.json | grep -E '"version": "[0-9]+.[0-9]+.[0-9]+"' | grep -Eo '[0-9]+.[0-9]+.[0-9]+' > build/version.txt + - name: "Create Sentry release" + if: ${{ (inputs.PUSH_RELEASE_TO_SENTRY) && (!github.event.act) }} + uses: getsentry/action-release@v1 + env: + SENTRY_AUTH_TOKEN: ${{ steps.secrets.outputs.SENTRY_TOKEN }} + SENTRY_ORG: sentry + SENTRY_PROJECT: pro + SENTRY_URL: https://sentry.passculture.team/ + with: + sourcemaps: ./build + working_directory: . + version: ${{ inputs.CHANNEL }} + url_prefix: "~" + - uses: FirebaseExtended/action-hosting-deploy@v0 + id: firebase-deploy + with: + repoToken: "${{ secrets.GITHUB_TOKEN }}" + firebaseServiceAccount: ${{ steps.secrets.outputs.FIREBASE_TOKEN }} + expires: ${{ inputs.EXPIRES }} + projectId: pc-native-${{ inputs.ENV }} + entryPoint: native + channelId: ${{ inputs.CHANNEL }} + - name: "Firebase Deployment URL" + run: | + echo "::notice:: Firebase deployment is available at : ${{ steps.firebase-deploy.outputs.details_url }}" \ No newline at end of file diff --git a/ACT.md b/ACT.md new file mode 100644 index 00000000000..b550ce4511c --- /dev/null +++ b/ACT.md @@ -0,0 +1,17 @@ +# ACT + +## Requirements + +- act +- github cli +- podman + +### Podman configuration + +```shell +podman machine init --cpus 4 --memory 8192 --now gha-act +``` + +## Local testing + +act -W .github/workflows/pr.yml -P ubuntu-22.04=catthehacker/ubuntu:full-22.04 -s GITHUB_TOKEN="$(gh auth token)" --eventpath act/event.json --env-file .env.testing diff --git a/__snapshots__/features/profile/pages/Achievements/Achievements.native.test.tsx.native-snap b/__snapshots__/features/profile/pages/Achievements/Achievements.native.test.tsx.native-snap index 7209cd76d47..e680cfe6d1c 100644 --- a/__snapshots__/features/profile/pages/Achievements/Achievements.native.test.tsx.native-snap +++ b/__snapshots__/features/profile/pages/Achievements/Achievements.native.test.tsx.native-snap @@ -383,7 +383,6 @@ exports[` should match snapshot 1`] = ` data={ [ { - "description": "Réserve ta première place de cinéma", "id": "FIRST_MOVIE_BOOKING", "illustration": { "$$typeof": Symbol(react.forward_ref), @@ -405,7 +404,6 @@ exports[` should match snapshot 1`] = ` "name": "Cinéphile en herbe", }, { - "description": "Tu as réservé ton premier livre", "id": "FIRST_BOOK_BOOKING", "illustration": { "$$typeof": Symbol(react.forward_ref), @@ -427,7 +425,6 @@ exports[` should match snapshot 1`] = ` "name": "Rat de bibliothèque", }, { - "description": "Tu as réservé ton premier atelier ou cours artistique", "id": "FIRST_ART_LESSON_BOOKING", "illustration": { "$$typeof": Symbol(react.forward_ref), @@ -449,8 +446,7 @@ exports[` should match snapshot 1`] = ` "name": "Se mettre à la pratique", }, { - "description": "Réserve du matériel créatif", - "id": "FIRST_INSTRUMENT_BOOKING", + "id": "FIRST_RECORDED_MUSIC_BOOKING", "illustration": { "$$typeof": Symbol(react.forward_ref), "attrs": [ @@ -468,11 +464,10 @@ exports[` should match snapshot 1`] = ` "withComponent": [Function], }, "isCompleted": false, - "name": "Artiste en devenir", + "name": "Badge non débloqué", }, { - "description": "Réserve ta première visite", - "id": "FIRST_MUSEUM_BOOKING", + "id": "FIRST_SHOW_BOOKING", "illustration": { "$$typeof": Symbol(react.forward_ref), "attrs": [ @@ -490,11 +485,10 @@ exports[` should match snapshot 1`] = ` "withComponent": [Function], }, "isCompleted": false, - "name": "Explorateur culturel", + "name": "Badge non débloqué", }, { - "description": "Abonne-toi à un média", - "id": "FIRST_NEWS_BOOKING", + "id": "FIRST_MUSEUM_BOOKING", "illustration": { "$$typeof": Symbol(react.forward_ref), "attrs": [ @@ -512,10 +506,9 @@ exports[` should match snapshot 1`] = ` "withComponent": [Function], }, "isCompleted": false, - "name": "Futur Hugo Décrypte", + "name": "Badge non débloqué", }, { - "description": "Réserve ton premier concert ou festival", "id": "FIRST_LIVE_MUSIC_BOOKING", "illustration": { "$$typeof": Symbol(react.forward_ref), @@ -534,11 +527,10 @@ exports[` should match snapshot 1`] = ` "withComponent": [Function], }, "isCompleted": false, - "name": "Premier Beat", + "name": "Badge non débloqué", }, { - "description": "Réserve ton premier CD ou vinyle", - "id": "FIRST_RECORDED_MUSIC_BOOKING", + "id": "FIRST_NEWS_BOOKING", "illustration": { "$$typeof": Symbol(react.forward_ref), "attrs": [ @@ -556,11 +548,10 @@ exports[` should match snapshot 1`] = ` "withComponent": [Function], }, "isCompleted": false, - "name": "Premier tour de platine", + "name": "Badge non débloqué", }, { - "description": "Réserve ton premier spectacle", - "id": "FIRST_SHOW_BOOKING", + "id": "FIRST_INSTRUMENT_BOOKING", "illustration": { "$$typeof": Symbol(react.forward_ref), "attrs": [ @@ -578,7 +569,7 @@ exports[` should match snapshot 1`] = ` "withComponent": [Function], }, "isCompleted": false, - "name": "Rideau rouge levé", + "name": "Badge non débloqué", }, { "description": "", @@ -1112,7 +1103,7 @@ exports[` should match snapshot 1`] = ` ] } > - Artiste en devenir + Badge non débloqué @@ -1248,7 +1239,7 @@ exports[` should match snapshot 1`] = ` ] } > - Explorateur culturel + Badge non débloqué @@ -1365,7 +1356,7 @@ exports[` should match snapshot 1`] = ` ] } > - Futur Hugo Décrypte + Badge non débloqué @@ -1501,7 +1492,7 @@ exports[` should match snapshot 1`] = ` ] } > - Premier Beat + Badge non débloqué @@ -1618,7 +1609,7 @@ exports[` should match snapshot 1`] = ` ] } > - Premier tour de platine + Badge non débloqué @@ -1754,7 +1745,7 @@ exports[` should match snapshot 1`] = ` ] } > - Rideau rouge levé + Badge non débloqué diff --git a/act/event.json b/act/event.json new file mode 100644 index 00000000000..6e572e0c751 --- /dev/null +++ b/act/event.json @@ -0,0 +1,9 @@ +{ + "act":true, + "inputs":{ + "ENV":"testing", + "PUSH_RELEASE_TO_SENTRY":"false", + "CHANNEL":"pr-testing-foo", + "REF":"master" + } +} \ No newline at end of file diff --git a/src/features/profile/components/Achievements/Badge.tsx b/src/features/profile/components/Achievements/Badge.tsx index 777f4d4cbab..fc1d3d54812 100644 --- a/src/features/profile/components/Achievements/Badge.tsx +++ b/src/features/profile/components/Achievements/Badge.tsx @@ -2,7 +2,6 @@ import React, { FC } from 'react' import styled, { useTheme } from 'styled-components/native' import { AchievementDetailsModal } from 'features/profile/components/Modals/AchievementDetailsModal' -import { useAchievementDetails } from 'features/profile/components/Modals/useAchievementDetails' import { AchievementId } from 'features/profile/pages/Achievements/AchievementData' import { useModal } from 'ui/components/modals/useModal' import { TouchableOpacity } from 'ui/components/TouchableOpacity' @@ -12,11 +11,11 @@ import { Spacer, TypoDS, getSpacing } from 'ui/theme' type BadgeProps = { id: AchievementId Illustration: React.FC + name: string isCompleted?: boolean } -export const Badge: FC = ({ Illustration, id, isCompleted }) => { - const achievement = useAchievementDetails(id) +export const Badge: FC = ({ Illustration, id, name, isCompleted }) => { const { visible, showModal, hideModal } = useModal(false) const theme = useTheme() @@ -29,7 +28,7 @@ export const Badge: FC = ({ Illustration, id, isCompleted }) => { - {achievement?.name} + {name} diff --git a/src/features/profile/components/Modals/AchievementDetailsModal.tsx b/src/features/profile/components/Modals/AchievementDetailsModal.tsx index 08e10945be6..ac70d8aa187 100644 --- a/src/features/profile/components/Modals/AchievementDetailsModal.tsx +++ b/src/features/profile/components/Modals/AchievementDetailsModal.tsx @@ -44,8 +44,12 @@ export const AchievementDetailsModal = ({ visible, hideModal, id }: Props) => { )} - {achievement.name} - + {achievement.completed ? ( + + {achievement.name} + + + ) : null} {achievement.completed ? achievement.descriptionUnlocked : achievement.descriptionLocked} diff --git a/src/features/profile/pages/Achievements/Achievements.tsx b/src/features/profile/pages/Achievements/Achievements.tsx index 843dbe6471b..08594ead311 100644 --- a/src/features/profile/pages/Achievements/Achievements.tsx +++ b/src/features/profile/pages/Achievements/Achievements.tsx @@ -29,7 +29,7 @@ const emptyBadge = { export const Achievements = () => { const { uniqueColors } = useTheme() - const { badges } = useAchievements({ + const categories = useAchievements({ achievements: mockAchievements, completedAchievements: mockCompletedAchievements, }) @@ -38,46 +38,32 @@ export const Achievements = () => { Mes Succès - {badges.map((badge) => { - const remainingAchievementsText = `${badge.remainingAchievements} badge${badge.remainingAchievements > 1 ? 's' : ''} restant` - - const completedAchievements = badge.achievements.filter((item) => item.isCompleted) - const incompleteAchievements = badge.achievements.filter((item) => !item.isCompleted) - - const sortedCompletedAchievements = [...completedAchievements].sort((a, b) => - a.name.localeCompare(b.name) - ) - - const sortedIncompleteAchievements = [...incompleteAchievements].sort((a, b) => - a.name.localeCompare(b.name) - ) - - let sortedAchievements = [...sortedCompletedAchievements, ...sortedIncompleteAchievements] - const oddAchievements = sortedAchievements.length % 2 !== 0 - if (oddAchievements) sortedAchievements = [...sortedAchievements, emptyBadge] + {categories.map((category) => { + const isOddBadges = category.badges.length % 2 !== 0 + const badges = isOddBadges ? [...category.badges, emptyBadge] : category.badges return ( - + - {achievementCategoryDisplayNames[badge.category]} + {achievementCategoryDisplayNames[category.id]} - {remainingAchievementsText} + {category.remainingAchievementsText} - {badge.progressText} + {category.progressText} item.id} contentContainerStyle={{ @@ -92,6 +78,7 @@ export const Achievements = () => { item.illustration ? ( diff --git a/src/features/profile/pages/Achievements/useAchievements.native.test.ts b/src/features/profile/pages/Achievements/useAchievements.native.test.ts index 7cf2918f846..da2f04150b5 100644 --- a/src/features/profile/pages/Achievements/useAchievements.native.test.ts +++ b/src/features/profile/pages/Achievements/useAchievements.native.test.ts @@ -10,7 +10,10 @@ import { userCompletedBookBooking, userCompletedMovieBooking, } from 'features/profile/pages/Achievements/AchievementData' -import { useAchievements } from 'features/profile/pages/Achievements/useAchievements' +import { + UseAchivementsProps, + useAchievements, +} from 'features/profile/pages/Achievements/useAchievements' import { renderHook } from 'tests/utils' import { BicolorTrophy, Trophy } from 'ui/svg/icons/Trophy' @@ -41,41 +44,41 @@ const testAchievement = { category: CombinedAchievementCategory.TEST, } +const testUseAchievements = ({ + achievements = [], + completedAchievements = [], +}: Partial = {}) => + renderHook(() => + useAchievements({ + achievements, + completedAchievements, + }) + ).result.current + describe('useAchievements', () => { it('should return empty array when there are no achievements', () => { - const { result } = renderHook(() => - useAchievements({ - achievements: [], - completedAchievements: [], - }) - ) - - const { badges } = result.current + const badges = testUseAchievements() expect(badges).toEqual([]) }) it('should return achievements grouped by category', () => { - const { result } = renderHook(() => - useAchievements({ - achievements: [firstArtLessonBooking, testAchievement as unknown as Achievement], - completedAchievements: [], - }) - ) - const { badges } = result.current + const badges = testUseAchievements({ + achievements: [firstArtLessonBooking, testAchievement as unknown as Achievement], + }) expect(badges).toEqual([ expect.objectContaining({ - category: CombinedAchievementCategory.FIRST_BOOKINGS, - achievements: [ + id: CombinedAchievementCategory.FIRST_BOOKINGS, + badges: [ expect.objectContaining({ id: CombinedAchievementId.FIRST_ART_LESSON_BOOKING, }), ], }), expect.objectContaining({ - category: CombinedAchievementCategory.TEST, - achievements: [ + id: CombinedAchievementCategory.TEST, + badges: [ expect.objectContaining({ id: 'TEST', }), @@ -85,18 +88,14 @@ describe('useAchievements', () => { }) it('achievement is NOT completed when user has not already completed it', () => { - const { result } = renderHook(() => - useAchievements({ - achievements: [firstArtLessonBooking, firstBookBooking], - completedAchievements: [], - }) - ) - const { badges } = result.current + const badges = testUseAchievements({ + achievements: [firstArtLessonBooking, firstBookBooking], + }) expect(badges).toEqual([ expect.objectContaining({ - category: CombinedAchievementCategory.FIRST_BOOKINGS, - achievements: [ + id: CombinedAchievementCategory.FIRST_BOOKINGS, + badges: [ expect.objectContaining({ id: CombinedAchievementId.FIRST_ART_LESSON_BOOKING, isCompleted: false, @@ -111,25 +110,110 @@ describe('useAchievements', () => { }) it('achievement is completed when user has already completed it', () => { - const { result } = renderHook(() => - useAchievements({ - achievements: [firstArtLessonBooking, firstBookBooking], - completedAchievements: [userCompletedArtLessonBooking, userCompletedBookBooking], - }) - ) - const { badges } = result.current + const badges = testUseAchievements({ + achievements: [firstArtLessonBooking, firstBookBooking], + completedAchievements: [userCompletedArtLessonBooking, userCompletedBookBooking], + }) expect(badges).toEqual([ expect.objectContaining({ - category: CombinedAchievementCategory.FIRST_BOOKINGS, - achievements: [ + id: CombinedAchievementCategory.FIRST_BOOKINGS, + badges: [ + expect.objectContaining({ + id: CombinedAchievementId.FIRST_BOOK_BOOKING, + isCompleted: true, + }), expect.objectContaining({ id: CombinedAchievementId.FIRST_ART_LESSON_BOOKING, isCompleted: true, }), + ], + }), + ]) + }) + + it('achievement name is "Badge non débloqué" when achievement is not completed', () => { + const badges = testUseAchievements({ + achievements: [firstArtLessonBooking, firstBookBooking], + completedAchievements: [], + }) + + expect(badges).toEqual([ + expect.objectContaining({ + id: CombinedAchievementCategory.FIRST_BOOKINGS, + badges: [ + expect.objectContaining({ + id: CombinedAchievementId.FIRST_ART_LESSON_BOOKING, + name: 'Badge non débloqué', + }), + expect.objectContaining({ + id: CombinedAchievementId.FIRST_BOOK_BOOKING, + name: 'Badge non débloqué', + }), + ], + }), + ]) + }) + + it('achievement completed name is the achievement name', () => { + const badges = testUseAchievements({ + achievements: [firstArtLessonBooking, firstBookBooking], + completedAchievements: [userCompletedArtLessonBooking, userCompletedBookBooking], + }) + + expect(badges).toEqual([ + expect.objectContaining({ + id: CombinedAchievementCategory.FIRST_BOOKINGS, + badges: [ + expect.objectContaining({ + id: CombinedAchievementId.FIRST_BOOK_BOOKING, + name: firstBookBooking.name, + }), + expect.objectContaining({ + id: CombinedAchievementId.FIRST_ART_LESSON_BOOKING, + name: firstArtLessonBooking.name, + }), + ], + }), + ]) + }) + + it('achivements are sorted by name', () => { + const badges = testUseAchievements({ + achievements: [firstArtLessonBooking, firstBookBooking], + completedAchievements: [userCompletedArtLessonBooking, userCompletedBookBooking], + }) + + expect(badges).toEqual([ + expect.objectContaining({ + id: CombinedAchievementCategory.FIRST_BOOKINGS, + badges: [ + expect.objectContaining({ + id: CombinedAchievementId.FIRST_BOOK_BOOKING, + }), + expect.objectContaining({ + id: CombinedAchievementId.FIRST_ART_LESSON_BOOKING, + }), + ], + }), + ]) + }) + + it('achievement completed is sorted before achievement not completed', () => { + const badges = testUseAchievements({ + achievements: [firstBookBooking, firstArtLessonBooking], + completedAchievements: [userCompletedArtLessonBooking], + }) + + expect(badges).toEqual([ + expect.objectContaining({ + id: CombinedAchievementCategory.FIRST_BOOKINGS, + badges: [ + expect.objectContaining({ + id: CombinedAchievementId.FIRST_ART_LESSON_BOOKING, + }), expect.objectContaining({ id: CombinedAchievementId.FIRST_BOOK_BOOKING, - isCompleted: true, }), ], }), @@ -138,53 +222,43 @@ describe('useAchievements', () => { describe('Category Achievements completion', () => { describe('Remaining achievements to complete', () => { - it('should return 2 when there are 2 achievements and no one is completed', () => { - const { result } = renderHook(() => - useAchievements({ - achievements: [firstArtLessonBooking, firstBookBooking], - completedAchievements: [], - }) - ) - const { badges } = result.current + it('should return "0 badge restant" when all achievements of category are completed', () => { + const badges = testUseAchievements({ + achievements: [firstArtLessonBooking, firstBookBooking], + completedAchievements: [userCompletedArtLessonBooking, userCompletedBookBooking], + }) expect(badges).toEqual([ expect.objectContaining({ - category: CombinedAchievementCategory.FIRST_BOOKINGS, - remainingAchievements: 2, + id: CombinedAchievementCategory.FIRST_BOOKINGS, + remainingAchievementsText: '0 badge restant', }), ]) }) - it('should return 1 when only 1 achievement is remaining', () => { - const { result } = renderHook(() => - useAchievements({ - achievements: [firstArtLessonBooking, firstBookBooking], - completedAchievements: [userCompletedArtLessonBooking], - }) - ) - const { badges } = result.current + it('should return "1 badge restants" when 1 achievement are not completed', () => { + const badges = testUseAchievements({ + achievements: [firstArtLessonBooking, firstBookBooking], + completedAchievements: [userCompletedArtLessonBooking], + }) expect(badges).toEqual([ expect.objectContaining({ - category: CombinedAchievementCategory.FIRST_BOOKINGS, - remainingAchievements: 1, + id: CombinedAchievementCategory.FIRST_BOOKINGS, + remainingAchievementsText: '1 badge restant', }), ]) }) - it('should return 0 when all achievement is completed', () => { - const { result } = renderHook(() => - useAchievements({ - achievements: [firstArtLessonBooking, firstBookBooking], - completedAchievements: [userCompletedArtLessonBooking, userCompletedBookBooking], - }) - ) - const { badges } = result.current + it('should return "2 badges restants" when 2 achievement are not completed', () => { + const badges = testUseAchievements({ + achievements: [firstArtLessonBooking, firstBookBooking], + }) expect(badges).toEqual([ expect.objectContaining({ - category: CombinedAchievementCategory.FIRST_BOOKINGS, - remainingAchievements: 0, + id: CombinedAchievementCategory.FIRST_BOOKINGS, + remainingAchievementsText: '2 badges restant', }), ]) }) @@ -192,77 +266,64 @@ describe('useAchievements', () => { describe('Achievements progression', () => { it('should be 1 when all achievements are completed', () => { - const { result } = renderHook(() => - useAchievements({ - achievements: [firstArtLessonBooking], - completedAchievements: [userCompletedArtLessonBooking], - }) - ) - const { badges } = result.current + const badges = testUseAchievements({ + achievements: [firstArtLessonBooking], + completedAchievements: [userCompletedArtLessonBooking], + }) expect(badges).toEqual([ expect.objectContaining({ - category: CombinedAchievementCategory.FIRST_BOOKINGS, + id: CombinedAchievementCategory.FIRST_BOOKINGS, progress: 1, }), ]) }) it('should be 0 when no achievements are completed', () => { - const { result } = renderHook(() => - useAchievements({ - achievements: [firstArtLessonBooking], - completedAchievements: [], - }) - ) - const { badges } = result.current + const badges = testUseAchievements({ + achievements: [firstArtLessonBooking], + }) expect(badges).toEqual([ expect.objectContaining({ - category: CombinedAchievementCategory.FIRST_BOOKINGS, + id: CombinedAchievementCategory.FIRST_BOOKINGS, progress: 0, }), ]) }) it('should be 0.5 when 1 achievement of 2 are completed', () => { - const { result } = renderHook(() => - useAchievements({ - achievements: [firstArtLessonBooking, firstBookBooking], - completedAchievements: [userCompletedArtLessonBooking], - }) - ) - const { badges } = result.current + const badges = testUseAchievements({ + achievements: [firstArtLessonBooking, firstBookBooking], + completedAchievements: [userCompletedArtLessonBooking], + }) expect(badges).toEqual([ expect.objectContaining({ - category: CombinedAchievementCategory.FIRST_BOOKINGS, + id: CombinedAchievementCategory.FIRST_BOOKINGS, progress: 0.5, }), ]) }) it('should be 0.75 when 3 achievement of 4 are completed', () => { - const { result } = renderHook(() => - useAchievements({ - achievements: [ - firstArtLessonBooking, - firstBookBooking, - firstInstrumentBooking, - firstMovieBooking, - ], - completedAchievements: [ - userCompletedArtLessonBooking, - userCompletedBookBooking, - userCompletedMovieBooking, - ], - }) - ) - const { badges } = result.current + const badges = testUseAchievements({ + achievements: [ + firstArtLessonBooking, + firstBookBooking, + firstInstrumentBooking, + firstMovieBooking, + ], + completedAchievements: [ + userCompletedArtLessonBooking, + userCompletedBookBooking, + userCompletedMovieBooking, + ], + }) expect(badges).toEqual([ expect.objectContaining({ - category: CombinedAchievementCategory.FIRST_BOOKINGS, + id: CombinedAchievementCategory.FIRST_BOOKINGS, progress: 0.75, }), ]) @@ -270,51 +331,41 @@ describe('useAchievements', () => { describe('text', () => { it('should return 0/2 when no achievements are completed', () => { - const { result } = renderHook(() => - useAchievements({ - achievements: [firstArtLessonBooking, firstBookBooking], - completedAchievements: [], - }) - ) - const { badges } = result.current + const badges = testUseAchievements({ + achievements: [firstArtLessonBooking, firstBookBooking], + }) expect(badges).toEqual([ expect.objectContaining({ - category: CombinedAchievementCategory.FIRST_BOOKINGS, + id: CombinedAchievementCategory.FIRST_BOOKINGS, progressText: '0/2', }), ]) }) it('should return 2/2 when all achievements are completed', () => { - const { result } = renderHook(() => - useAchievements({ - achievements: [firstArtLessonBooking, firstBookBooking], - completedAchievements: [userCompletedArtLessonBooking, userCompletedBookBooking], - }) - ) - const { badges } = result.current + const badges = testUseAchievements({ + achievements: [firstArtLessonBooking, firstBookBooking], + completedAchievements: [userCompletedArtLessonBooking, userCompletedBookBooking], + }) expect(badges).toEqual([ expect.objectContaining({ - category: CombinedAchievementCategory.FIRST_BOOKINGS, + id: CombinedAchievementCategory.FIRST_BOOKINGS, progressText: '2/2', }), ]) }) - it('should return 50% when 1 achievement of 2 are completed', () => { - const { result } = renderHook(() => - useAchievements({ - achievements: [firstArtLessonBooking, firstBookBooking], - completedAchievements: [userCompletedArtLessonBooking], - }) - ) - const { badges } = result.current + it('should return 1/2 when 1 achievement of 2 are completed', () => { + const badges = testUseAchievements({ + achievements: [firstArtLessonBooking, firstBookBooking], + completedAchievements: [userCompletedArtLessonBooking], + }) expect(badges).toEqual([ expect.objectContaining({ - category: CombinedAchievementCategory.FIRST_BOOKINGS, + id: CombinedAchievementCategory.FIRST_BOOKINGS, progressText: '1/2', }), ]) diff --git a/src/features/profile/pages/Achievements/useAchievements.ts b/src/features/profile/pages/Achievements/useAchievements.ts index 74bb72468f3..4b61f86eca8 100644 --- a/src/features/profile/pages/Achievements/useAchievements.ts +++ b/src/features/profile/pages/Achievements/useAchievements.ts @@ -6,73 +6,85 @@ import { } from 'features/profile/pages/Achievements/AchievementData' import { AccessibleIcon } from 'ui/svg/icons/types' -type Badges = { - category: AchievementCategory - remainingAchievements: number +type Categories = { + id: AchievementCategory + remainingAchievementsText: string progress: number progressText: string - achievements: { + badges: { id: AchievementId name: string - description: string illustration: React.FC isCompleted: boolean }[] }[] -type Props = { +export type UseAchivementsProps = { achievements: Achievement[] completedAchievements: UserAchievement[] } -export const useAchievements = ({ achievements, completedAchievements }: Props) => { - const badges: Badges = achievements.reduce((acc, achievement) => { - const category = acc.find((badge) => badge.category === achievement.category) - const isCompleted = completedAchievements.some((u) => u.id === achievement.id) - - if (category) { - category.achievements.push({ - id: achievement.id, - name: achievement.name, - description: isCompleted ? achievement.descriptionUnlocked : achievement.descriptionLocked, - illustration: isCompleted - ? achievement.illustrationUnlocked - : achievement.illustrationLocked, - isCompleted, - }) - - if (!isCompleted) { - category.remainingAchievements++ - } - - const actualAchievements = category.achievements.length - category.remainingAchievements - category.progress = actualAchievements / category.achievements.length - category.progressText = `${actualAchievements}/${category.achievements.length}` - return acc +export const useAchievements = ({ + achievements, + completedAchievements, +}: UseAchivementsProps): Categories => + getAchievementsCategories(achievements).map(createCategory(achievements, completedAchievements)) + +const getAchievementsCategories = (achievements: Achievement[]) => + Array.from(new Set(achievements.map((achievement) => achievement.category))) + +const isAchievementCompleted = ( + achievement: Achievement, + completedAchievements: UserAchievement[] +) => completedAchievements.some((u) => u.id === achievement.id) + +const getCompletedAchievements = ( + achievements: Achievement[], + completedAchievements: UserAchievement[] +) => + achievements.filter((achievement) => isAchievementCompleted(achievement, completedAchievements)) + +const getAchievementsByCategory = (achievements: Achievement[], category: AchievementCategory) => + achievements.filter((achievement) => achievement.category === category) + +const createCategory = + (achievements: Achievement[], completedAchievements: UserAchievement[]) => + (category: AchievementCategory) => { + const categoryAchievements = getAchievementsByCategory(achievements, category) + + const completedCategoryAchievements = getCompletedAchievements( + categoryAchievements, + completedAchievements + ) + + const remainingAchievements = categoryAchievements.length - completedCategoryAchievements.length + + const badges = categoryAchievements.map(createBadge(completedAchievements)) + + const completedBadges = badges + .filter((a) => a.isCompleted) + .sort((a, b) => a.name.localeCompare(b.name)) + + const uncompletedBadges = badges.filter((a) => !a.isCompleted) + + return { + id: category, + progress: completedCategoryAchievements.length / categoryAchievements.length, + progressText: `${completedCategoryAchievements.length}/${categoryAchievements.length}`, + remainingAchievementsText: `${remainingAchievements} badge${remainingAchievements > 1 ? 's' : ''} restant`, + badges: [...completedBadges, ...uncompletedBadges], } + } + +const LOCKED_BADGE_NAME = 'Badge non débloqué' - acc.push({ - category: achievement.category, - progress: isCompleted ? 1 : 0, - progressText: isCompleted ? '100%' : '0%', - remainingAchievements: isCompleted ? 0 : 1, - - achievements: [ - { - id: achievement.id, - name: achievement.name, - description: achievement.descriptionLocked, - illustration: isCompleted - ? achievement.illustrationUnlocked - : achievement.illustrationLocked, - isCompleted, - }, - ], - }) - return acc - }, [] as Badges) +const createBadge = (completedAchievements: UserAchievement[]) => (achievement: Achievement) => { + const isCompleted = isAchievementCompleted(achievement, completedAchievements) return { - badges, + id: achievement.id, + name: isCompleted ? achievement.name : LOCKED_BADGE_NAME, + illustration: isCompleted ? achievement.illustrationUnlocked : achievement.illustrationLocked, + isCompleted, } }