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,
}
}