diff --git a/.github/workflows/build-docker.yaml b/.github/workflows/build-docker.yaml new file mode 100644 index 000000000..088845884 --- /dev/null +++ b/.github/workflows/build-docker.yaml @@ -0,0 +1,82 @@ +name: Build docker + +on: + push: + branches: + - develop + - staging + - main + +env: + IMAGE_NAME: europe-docker.pkg.dev/jumper-g-artifacts/docker-jumper-exchange/jumpex + +jobs: + build-docker: + runs-on: ubuntu-latest + + # id token + permissions: + contents: "read" + id-token: "write" + + steps: + - # check out the repository + name: Checkout + uses: actions/checkout@v2 + + - name: Set current date as env variable + run: echo "UNIQ_ID=$(date +'%y%m%d')-${GITHUB_SHA:0:7}" >> $GITHUB_ENV + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Cache Docker layers + uses: actions/cache@v3 + with: + path: | + /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + + - name: Authenticate to Google Cloud + id: gcpauth + uses: google-github-actions/auth@v2 + with: + create_credentials_file: 'true' + workload_identity_provider: 'projects/800848389157/locations/global/workloadIdentityPools/github/providers/github' + service_account: 'artifact-deployer@jumper-g-management.iam.gserviceaccount.com' + + - # login to gcp + name: login + run: |- + gcloud auth login --brief --cred-file="${{ steps.gcpauth.outputs.credentials_file_path }}" + gcloud auth configure-docker europe-docker.pkg.dev + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v4 + with: + images: ${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=branch,prefix=${{ env.UNIQ_ID }}- + + - name: Build and push Docker image + uses: docker/build-push-action@v3 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + platforms: linux/amd64,linux/arm64 + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: | + type=local,src=/tmp/.buildx-cache + cache-to: | + type=local,dest=/tmp/.buildx-cache + build-args: | + ENV_FILE=${{ github.ref_name == 'main' && '.env.production' || github.ref_name == 'develop' && '.env.development' || format('.env.{0}', github.ref_name) }} + ENV_NAME=${{ github.ref_name == 'main' && 'prod' || github.ref_name }} \ No newline at end of file diff --git a/src/components/Blog/CTAs/InstructionsAccordion/InstructionsAccordionItem.style.ts b/src/components/Blog/CTAs/InstructionsAccordion/InstructionsAccordionItem.style.ts index 9ff708c42..933044da6 100644 --- a/src/components/Blog/CTAs/InstructionsAccordion/InstructionsAccordionItem.style.ts +++ b/src/components/Blog/CTAs/InstructionsAccordion/InstructionsAccordionItem.style.ts @@ -1,6 +1,6 @@ import { getContrastAlphaColor } from '@/utils/colors'; import type { Breakpoint } from '@mui/material'; -import { Box, Typography, styled } from '@mui/material'; +import { Box, Typography, alpha, styled } from '@mui/material'; import { IconButtonTertiary } from 'src/components/IconButton.style'; export const InstructionsAccordionItemContainer = styled(Box)(({ theme }) => ({ @@ -77,6 +77,12 @@ export const InstructionsAccordionToggle = styled(IconButtonTertiary)( export const InstructionsAccordionItemLabel = styled(Box)(({ theme }) => ({ marginLeft: theme.spacing(2), + color: alpha( + theme.palette.mode === 'light' + ? theme.palette.black.main + : theme.palette.white.main, + 0.75, + ), fontWeight: 600, fontSize: '18px', lineHeight: '32px', diff --git a/src/components/Blog/CustomRichBlocks.style.ts b/src/components/Blog/CustomRichBlocks.style.ts index 740e68529..a8b942c9b 100644 --- a/src/components/Blog/CustomRichBlocks.style.ts +++ b/src/components/Blog/CustomRichBlocks.style.ts @@ -8,6 +8,7 @@ interface BlogParagraphProps extends TypographyProps { underline?: boolean; strikethrough?: boolean; italic?: boolean; + quote?: boolean; } export const BlogParagraph = styled(Typography, { @@ -15,8 +16,16 @@ export const BlogParagraph = styled(Typography, { prop !== 'bold' && prop !== 'underline' && prop !== 'italic' && - prop !== 'strikethrough', -})(({ theme, bold, underline, strikethrough, italic }) => { + prop !== 'strikethrough' && + prop !== 'quote', +})(({ + theme, + bold, + underline, + strikethrough, + italic, + quote, +}) => { const textDecoration = underline ? 'underline' : strikethrough @@ -36,6 +45,10 @@ export const BlogParagraph = styled(Typography, { fontSize: '18px', lineHeight: '32px', margin: theme.spacing(2, 0), + ...(quote && { + fontSize: '20px', + fontStyle: 'italic', + }), }; }); diff --git a/src/components/Blog/CustomRichBlocks.tsx b/src/components/Blog/CustomRichBlocks.tsx index 9501e55ce..856312f2a 100644 --- a/src/components/Blog/CustomRichBlocks.tsx +++ b/src/components/Blog/CustomRichBlocks.tsx @@ -44,6 +44,30 @@ interface WidgetRouteSettings toChain?: string; } +interface ParagraphProps { + text: string; + bold?: boolean; + italic?: boolean; + underline?: boolean; + strikethrough?: boolean; + [key: string]: any; +} + +interface QuoteProps { + text: string; + [key: string]: any; +} + +interface RichElement { + key: string | null; + props: T; + ref: any; + [key: string]: any; +} + +type ParagraphElement = RichElement; +type QuoteElement = RichElement; + export const CustomRichBlocks = ({ id, baseUrl, @@ -56,6 +80,7 @@ export const CustomRichBlocks = ({ baseUrl ? ( ) : undefined, + heading: ({ children, level, @@ -102,7 +127,21 @@ export const CustomRichBlocks = ({ ); } }, - paragraph: ({ children }: any) => { + + quote: ({ children }: { children: QuoteElement[] }) => { + return children.map((quote) => { + return ( + + + {quote.props.text} + + + ); + }); + }, + + paragraph: ({ children }: ParagraphElement) => { + console.log('PARAGRAPH', children); if (children[0].props.text.includes(' - {children.map((el: any, index: number) => { + {children.map((el: ParagraphElement, index: number) => { if (el.props.text || el.props.text !== '') { if (el.props.content?.type === 'link') { return ( diff --git a/src/components/Superfest/AvailableMissionsList/AvailableMissionsList.tsx b/src/components/Superfest/AvailableMissionsList/AvailableMissionsList.tsx index 105ed8937..53efa9657 100644 --- a/src/components/Superfest/AvailableMissionsList/AvailableMissionsList.tsx +++ b/src/components/Superfest/AvailableMissionsList/AvailableMissionsList.tsx @@ -32,12 +32,14 @@ interface AvailableMissionsListProps { quests?: Quest[]; pastCampaigns?: string[]; loading: boolean; + isJumperTurtleMember?: boolean; } export const AvailableMissionsList = ({ quests, pastCampaigns, loading, + isJumperTurtleMember, }: AvailableMissionsListProps) => { const isMobile = useMediaQuery((theme: Theme) => theme.breakpoints.down('md'), @@ -107,6 +109,8 @@ export const AvailableMissionsList = ({ const rewards = quest.attributes.CustomInformation?.['rewards']; const rewardType = quest.attributes?.CustomInformation?.['rewardType']; + const missionType = + quest?.attributes?.CustomInformation?.['missionType']; const rewardRange = quest.attributes?.CustomInformation?.['rewardRange']; const chains = quest.attributes.CustomInformation?.['chains']; @@ -146,6 +150,13 @@ export const AvailableMissionsList = ({ if (rewardsIds && pastCampaigns) { completed = checkInclusion(pastCampaigns, rewardsIds); } + if ( + missionType && + missionType === 'turtle_signature' && + isJumperTurtleMember + ) { + completed = true; + } return ( { }); const NFTimage = - chain === 'box' && (claimInfo.isClaimable || claimInfo.isClaimed) - ? LAST_NFT_IMAGE - : image; + chain === 'box' && claimInfo.isClaimed ? LAST_NFT_IMAGE : image; + + // chain === 'box' && (claimInfo.isClaimable || claimInfo.isClaimed) + // ? LAST_NFT_IMAGE + // : image; async function handleClick() { try { @@ -171,50 +174,91 @@ export const NFTCard = ({ image, chain, bgColor, typoColor }: NFTCardProps) => { ); } - if (claimInfo?.isClaimable) { - return ( - - {chain} - - - - - ); - } + // if (claimInfo?.isClaimable) { + // return ( + // + // {chain} + // + // + // + // + // ); + // } + // return ( + // + // {chain} + // + // + // + // + // ); + + // temporary fix return ( { height="288" /> - + + ); diff --git a/src/components/Superfest/Superfest.tsx b/src/components/Superfest/Superfest.tsx index 0c58ac5e9..17e95f465 100644 --- a/src/components/Superfest/Superfest.tsx +++ b/src/components/Superfest/Superfest.tsx @@ -7,6 +7,7 @@ import { AvailableMissionsList } from './AvailableMissionsList/AvailableMissions import { ActiveSuperfestMissionsCarousel } from './ActiveSuperfestMissionsCarousel/ActiveSuperfestMissionsCarousel'; import { useOngoingFestMissions } from 'src/hooks/useOngoingFestMissions'; import { useMerklRewards } from 'src/hooks/useMerklRewardsOnSpecificToken'; +import { useTurtleMember } from 'src/hooks/useTurtleMember'; export const Superfest = () => { //HOOKS @@ -22,6 +23,13 @@ export const Superfest = () => { rewardChainId: 10, userAddress: account?.address, }); + const { + isMember, + isJumperMember, + isSuccess: isMemberCheckSuccess, + } = useTurtleMember({ + userAddress: account?.address, + }); return ( @@ -51,6 +59,9 @@ export const Superfest = () => { quests={quests} loading={isQuestLoading} pastCampaigns={pastCampaigns} + isJumperTurtleMember={ + isMember && isJumperMember && isMemberCheckSuccess + } /> diff --git a/src/components/Superfest/SuperfestPage/Banner/Banner.tsx b/src/components/Superfest/SuperfestPage/Banner/Banner.tsx index 453646645..775ef716d 100644 --- a/src/components/Superfest/SuperfestPage/Banner/Banner.tsx +++ b/src/components/Superfest/SuperfestPage/Banner/Banner.tsx @@ -25,6 +25,7 @@ interface SuperfestMissionPageVar { quest: Quest; baseUrl: string; pastCampaigns: string[]; + isRewardCompleted?: boolean; } export interface Chain { @@ -36,6 +37,7 @@ export const BannerBox = ({ quest, baseUrl, pastCampaigns, + isRewardCompleted, }: SuperfestMissionPageVar) => { const isMobile = useMediaQuery((theme: Theme) => theme.breakpoints.down('md'), @@ -55,6 +57,9 @@ export const BannerBox = ({ if (rewardsIds && pastCampaigns) { completed = checkInclusion(pastCampaigns, rewardsIds); } + if (isRewardCompleted) { + completed = true; + } return ( <> diff --git a/src/components/Superfest/SuperfestPage/SuperfestMisisonPage.tsx b/src/components/Superfest/SuperfestPage/SuperfestMisisonPage.tsx index 14e2b4241..886691194 100644 --- a/src/components/Superfest/SuperfestPage/SuperfestMisisonPage.tsx +++ b/src/components/Superfest/SuperfestPage/SuperfestMisisonPage.tsx @@ -11,6 +11,7 @@ import { InformationAlertBox } from './InformationBox/InformationAlertBox'; import { useMerklRewards } from 'src/hooks/useMerklRewardsOnSpecificToken'; import { useAccounts } from '@/hooks/useAccounts'; import { useMissionsAPY } from 'src/hooks/useMissionsAPY'; +import { useTurtleMember } from 'src/hooks/useTurtleMember'; interface SuperfestMissionPageVar { quest: Quest; @@ -34,7 +35,13 @@ export const SuperfestMissionPage = ({ rewardChainId: 10, userAddress: account?.address, }); - + const { + isMember, + isJumperMember, + isSuccess: isMemberCheckSuccess, + } = useTurtleMember({ + userAddress: account?.address, + }); const { isLoading, isSuccess, CTAsWithAPYs } = useMissionsAPY(CTAs); return ( @@ -47,6 +54,12 @@ export const SuperfestMissionPage = ({ quest={quest} baseUrl={baseUrl} pastCampaigns={pastCampaigns} + isRewardCompleted={ + missionType === 'turtle_signature' && + isMemberCheckSuccess && + isMember && + isJumperMember + } /> {/* Big CTA */} { const theme = useTheme(); @@ -21,10 +21,10 @@ export const useWidgetTheme = (): PartnerTheme => { config: { appearance: theme.palette.mode, theme: { - //todo: fix typography - // typography: { - // fontFamily: theme.typography, - // }, + // @ts-expect-error + typography: { + fontFamily: theme.typography.fontFamily, + }, container: { borderRadius: '12px', maxWidth: '100%', diff --git a/src/hooks/querries/superfestNFT.ts b/src/hooks/querries/superfestNFT.ts index eda0a61d8..3d394debe 100644 --- a/src/hooks/querries/superfestNFT.ts +++ b/src/hooks/querries/superfestNFT.ts @@ -7,6 +7,7 @@ export const availableNFT = gql` signature: "" # provide a test signature campaignID: $campaignID # campaign hash id address: $address # user address + mintCount: 1 } ) { allow # Is allow user claim nft diff --git a/src/hooks/useTurtleMember.ts b/src/hooks/useTurtleMember.ts index 2b9548bde..3f8136b79 100644 --- a/src/hooks/useTurtleMember.ts +++ b/src/hooks/useTurtleMember.ts @@ -6,14 +6,18 @@ interface UseTurtleProps { interface UseTurtleRes { isMember?: boolean; + isJumperMember?: boolean; isLoading: boolean; isSuccess: boolean; } +const JUMPER_REF = 'JUMPER'; + export const useTurtleMember = ({ userAddress, }: UseTurtleProps): UseTurtleRes => { const TURTLE_CHECK_API = `https://points.turtle.club/user/${userAddress}/exists`; + const TURTLE_REFCHECK_API = `https://points.turtle.club/referral/${userAddress}`; const { data, isSuccess, isLoading } = useQuery({ queryKey: ['turtleMemberCheck'], @@ -33,5 +37,32 @@ export const useTurtleMember = ({ refetchInterval: 1000 * 60 * 60, }); - return { isMember: data, isSuccess, isLoading }; + const { + data: refCheck, + isSuccess: refCheckIsSuccess, + isLoading: refCheckIsLoading, + } = useQuery({ + queryKey: ['turtleMemberRefCheck'], + queryFn: async () => { + try { + const response = await fetch(TURTLE_REFCHECK_API); + const result = await response.json(); + if (result && result.used_referral) { + return result.used_referral === JUMPER_REF; + } + return false; + } catch (err) { + console.log(err); + } + }, + enabled: !!userAddress, + refetchInterval: 1000 * 60 * 60, + }); + + return { + isMember: data, + isJumperMember: refCheck, + isSuccess: isSuccess && refCheckIsSuccess, + isLoading: isLoading && refCheckIsLoading, + }; }; diff --git a/tests/e2e.spec.ts b/tests/e2e.spec.ts index a4bc4edea..33679a873 100644 --- a/tests/e2e.spec.ts +++ b/tests/e2e.spec.ts @@ -4,7 +4,9 @@ import { itemInMenu, tabInHeader, openMainMenu, + expectMenuToBeVisible, } from './testData/commonFunctions'; +import values from '../tests/testData/values.json'; test.describe('Jumper full e2e flow', () => { test.beforeEach(async ({ page }) => { @@ -62,67 +64,72 @@ test.describe('Jumper full e2e flow', () => { }) => { // await closeWelcomeScreen(page); await openMainMenu(page); - await expect(page.getByRole('menu')).toBeVisible(); + await expectMenuToBeVisible(page); await expect(page.getByRole('menuitem')).toHaveCount(11); await page.locator('body').click(); await expect(page.getByRole('menu')).not.toBeVisible(); }); - test('Should be able to navigate to profile', async ({ page }) => { + test('Should be able to navigate to profile and open Explore Fluid Mission', async ({ + page,context + }) => { let profileUrl = `${await page.url()}profile/`; // await closeWelcomeScreen(page); await openMainMenu(page); - await expect(page.getByRole('menu')).toBeVisible(); + await expectMenuToBeVisible(page); await itemInMenu(page, 'Jumper Profile'); expect(await page.url()).toBe(profileUrl); - await page.locator('.profile-page').isVisible({ timeout: 15000 }); + await page.locator('.profile-page').isVisible(); + await page + .locator('xpath=//p[normalize-space(text())="Explore Fluid"]') + .click(); + const newPage = await context.waitForEvent('page'); + expect(newPage.url()).toBe(values.exploreFluidURL); }); test('Should be able to navigate to jumper learn', async ({ page }) => { let learnUrl = `${await page.url()}learn/`; // await closeWelcomeScreen(page); await openMainMenu(page); - await expect(page.getByRole('menu')).toBeVisible(); + await expectMenuToBeVisible(page); await itemInMenu(page, 'Jumper Learn'); - expect(await page.url()).toBe(learnUrl); - await page.waitForLoadState('load', { timeout: 15000 }); + await page.waitForLoadState('load'); await page.locator('.learn-page').isVisible(); }); - test('Should be able to navigate to lifi explorer', async ({ page }) => { + test('Should be able to navigate to LI.FI Scan', async ({ page }) => { // await closeWelcomeScreen(page); await openMainMenu(page); - await expect(page.getByRole('menu')).toBeVisible(); + await expectMenuToBeVisible(page); await itemInMenu(page, 'Jumper Scan'); // const newPage = await page.waitForEvent('popup', { timeout: 15000 }); - expect(page).toHaveURL('http://localhost:3000/scan/'); + expect(page).toHaveURL(values.localJumperScanURL); }); - test('should be able to navigate to supefest', async ({ page }) => { + + test('Should be able to navigate to Supefest', async ({ page }) => { const learnMoreButton = page.locator('#learn-more-button'); await openMainMenu(page); await itemInMenu(page, 'Superfest Festival'); await expect(learnMoreButton).toBeVisible(); - await expect(page).toHaveURL('http://localhost:3000/superfest/'); + await expect(page).toHaveURL(values.localSuperfestURL); }); test('Should be able to navigate to X', async ({ page, context }) => { - let xUrl = 'https://x.com/JumperExchange'; // await closeWelcomeScreen(page); await openMainMenu(page); - await expect(page.getByRole('menu')).toBeVisible(); + await expectMenuToBeVisible(page); await page.getByRole('link', { name: 'X', exact: true }).click(); const newPage = await context.waitForEvent('page'); - expect(newPage.url()).toBe(xUrl); + expect(newPage.url()).toBe(values.xUrl); }); test('Should be able to navigate to Discord', async ({ page, context }) => { - let discordUrl = 'https://discord.com/invite/jumperexchange'; // await closeWelcomeScreen(page); await openMainMenu(page); - await expect(page.getByRole('menu')).toBeVisible(); + await expectMenuToBeVisible(page); await page.getByRole('link', { name: 'Discord' }).click(); const newPage = await context.waitForEvent('page'); - expect(newPage.url()).toBe(discordUrl); + expect(newPage.url()).toBe(values.discordURL); }); test('API test - Feature Cards', async ({ request }) => { const apiURL = 'https://strapi.li.finance/api/feature-cards'; diff --git a/tests/testData/commonFunctions.ts b/tests/testData/commonFunctions.ts index f73f0b355..522e99d80 100644 --- a/tests/testData/commonFunctions.ts +++ b/tests/testData/commonFunctions.ts @@ -1,4 +1,4 @@ -import type { Page } from '@playwright/test'; +import { expect , Page } from '@playwright/test'; export async function findTheBestRoute(page) { await page.getByRole('heading', { name: 'Find the best route' }); @@ -18,3 +18,7 @@ export async function closeWelcomeScreen(page: Page) { export async function tabInHeader(page, name: string) { await page.getByRole('tab', { name }).click(); } + +export async function expectMenuToBeVisible(page){ + await expect(page.getByRole('menu')).toBeVisible(); +} \ No newline at end of file diff --git a/tests/testData/values.json b/tests/testData/values.json new file mode 100644 index 000000000..8419d000c --- /dev/null +++ b/tests/testData/values.json @@ -0,0 +1,7 @@ +{ + "localJumperScanURL":"http://localhost:3000/scan/", + "localSuperfestURL":"http://localhost:3000/superfest/", + "xUrl": "https://x.com/JumperExchange", + "discordURL": "https://discord.com/invite/jumperexchange", + "exploreFluidURL":"https://jumper.exchange/superfest/rewards-from-fluid-on-base/" +} \ No newline at end of file