diff --git a/.env.local.template b/.env.local.template index cce1b3ef7..5c9e7f5dc 100644 --- a/.env.local.template +++ b/.env.local.template @@ -11,5 +11,8 @@ SUPABASE_DB_URL=postgresql://postgres:postgres@localhost:54322/postgres SUPABASE_SERVICE_ROLE=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU SUPABASE_JWT_SECRET=super-secret-jwt-token-with-at-least-32-characters-long NEXT_PUBLIC_MAINNET_CHAIN_ID=1337 -NEXT_PUBLIC_BASE_MAINNET_CHAIN_ID=845337 +NEXT_PUBLIC_BASE_CHAIN_ID=845337 +NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=33fe0df7cd4f445f4e2ba7e0fd6ed314 SNAPLET_HASH_KEY=sendapp +SEND_ACCOUNT_FACTORY_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 +SECRET_SHOP_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 diff --git a/.github/actions/deploy/action.yml b/.github/actions/deploy/action.yml index 8cd944667..81984d8aa 100644 --- a/.github/actions/deploy/action.yml +++ b/.github/actions/deploy/action.yml @@ -23,6 +23,10 @@ inputs: supabase-db-password: description: "The Supabase database password to use for deployment." required: true + qa-notification-url: + description: "The URL to use for notifying QA of a new deployment." + required: false + default: "" runs: using: "composite" steps: @@ -47,6 +51,7 @@ runs: id: public-hostname run: echo "public-hostname=sendapp-${{steps.extract-branch.outputs.branch}}-0xsend.vercel.app" >> $GITHUB_OUTPUT - name: Vercel Deploy + id: vercel-deploy uses: ./.github/actions/vercel-deploy with: vercel-token: ${{ inputs.vercel-token }} @@ -56,3 +61,15 @@ runs: production: ${{ inputs.production }} env: YARN_ENABLE_HARDENED_MODE: "0" + - name: Notify QA + if: ${{ inputs.qa-notification-url != '' }} + shell: bash + run: | + sha=$(git rev-parse HEAD | cut -c1-7) + summary=$(git log -1 --pretty=%B | head -n 1) + if [ "${{ inputs.production }}" = "true" ]; then + data=$(printf "New production deployment is available. Summary: %s %s\n\nUnique URL: %s\n\nPublic Hostname: %s" "$summary" "$sha" "${{ steps.vercel-deploy.outputs.deployment-url }}" "https://${{ steps.public-hostname.outputs.public-hostname }}") + else + data=$(printf "New staging deployment is available. Summary: %s %s\n\nUnique URL: %s\n\nPublic Hostname: %s" "$summary" "$sha" "${{ steps.vercel-deploy.outputs.deployment-url }}" "https://${{ steps.public-hostname.outputs.public-hostname }}") + fi + curl -X POST -d "$data" "${{ inputs.qa-notification-url }}" diff --git a/.github/actions/install-tilt/action.yml b/.github/actions/install-tilt/action.yml new file mode 100644 index 000000000..d9dbe9b91 --- /dev/null +++ b/.github/actions/install-tilt/action.yml @@ -0,0 +1,8 @@ +name: "Install Tilt" +runs: + using: "composite" + steps: + - name: Install Tilt + id: tilt + shell: bash + run: curl -fsSL https://raw.githubusercontent.com/tilt-dev/tilt/master/scripts/install.sh | bash diff --git a/.github/actions/setup-env/action.yml b/.github/actions/setup-env/action.yml index e1221336a..ca1c4fb01 100644 --- a/.github/actions/setup-env/action.yml +++ b/.github/actions/setup-env/action.yml @@ -17,6 +17,10 @@ inputs: description: "Whether to run Yarn install." required: false default: "true" + build-nextjs: + description: "Whether to build Next.js." + required: false + default: "false" runs: using: "composite" steps: @@ -38,3 +42,16 @@ runs: if: ${{ inputs.yarn-install == 'true' }} shell: bash run: yarn install --immutable + - name: Build Next.js + if: ${{ inputs.build-nextjs == 'true' }} + shell: bash + run: | + cp .env.local.template .env.local + yarn web:prod + rm .env.local + - name: Save Next.js + if: ${{ inputs.build-nextjs == 'true' }} + uses: actions/upload-artifact@v4 + with: + name: nextjs-build + path: ${{ github.workspace }}/apps/next/.next/ diff --git a/.github/actions/vercel-deploy/action.yml b/.github/actions/vercel-deploy/action.yml index 91298d749..fa34f40b8 100644 --- a/.github/actions/vercel-deploy/action.yml +++ b/.github/actions/vercel-deploy/action.yml @@ -10,6 +10,14 @@ inputs: vercel-project-id: description: "The Vercel project ID to use for deployment." required: true + vercel-scope: + description: "The Vercel scope to use for deployment." + required: false + default: "0xsend" + deploy-preview-extra-args: + description: "The Vercel extra args to add to the deploy preview command. e.g. '-e NODE_ENV=production -e API_URL=https://api.example.com'" + required: false + default: "" public-hostname: description: "The public hostname alias to use for the deployment." required: false @@ -21,51 +29,100 @@ inputs: outputs: deployment-url: description: "The URL of the deployment." - value: ${{ steps.deploy.outputs.deployment_url }} + value: ${{ steps.deploy-preview.outputs.deployment_url }} runs: using: "composite" steps: - name: Mask Vercel Token shell: bash - run: echo "::add-mask::${{ inputs.vercel-token }}" - - name: Switch to Vercel Send team + run: | + echo "::add-mask::${{ inputs.vercel-token }}" + - name: Switch to Vercel team + shell: bash + run: bunx vercel --token=${{ inputs.vercel-token }} team switch ${{ inputs.vercel-scope }} + env: + VERCEL_ORG_ID: ${{ inputs.vercel-org-id }} + VERCEL_PROJECT_ID: ${{ inputs.vercel-project-id }} + # BEGIN Preview Environment + - name: Setup Preview Environment + if: ${{ inputs.production != 'true' }} + shell: bash + run: | + bunx vercel --token=${{ inputs.vercel-token }} -S ${{ inputs.vercel-scope }} pull --yes --environment=preview + cp .vercel/.env.preview.local .env.local + env: + VERCEL_ORG_ID: ${{ inputs.vercel-org-id }} + VERCEL_PROJECT_ID: ${{ inputs.vercel-project-id }} + - name: Build Preview Project Artifacts + if: ${{ inputs.production != 'true' }} shell: bash - run: bunx vercel --token=${{ inputs.vercel-token }} team switch 0xsend + run: bunx vercel --token=${{ inputs.vercel-token }} -S ${{ inputs.vercel-scope }} build env: VERCEL_ORG_ID: ${{ inputs.vercel-org-id }} VERCEL_PROJECT_ID: ${{ inputs.vercel-project-id }} - - name: Pull Vercel Environment Information + - name: Deploy Preview + if: ${{ inputs.production != 'true' }} + id: deploy-preview shell: bash - run: bunx vercel --token=${{ inputs.vercel-token }} pull --yes --environment=preview + run: | + bunx vercel --token=${{ inputs.vercel-token }} -S ${{ inputs.vercel-scope }} deploy --prebuilt ${{inputs.deploy-preview-extra-args}} > deployment-url.txt + echo "deployment_url=$(cat deployment-url.txt)" >> $GITHUB_OUTPUT + echo ::notice::Deployment URL: $(cat deployment-url.txt) + env: + VERCEL_ORG_ID: ${{ inputs.vercel-org-id }} + VERCEL_PROJECT_ID: ${{ inputs.vercel-project-id }} + VERCEL_GIT_COMMIT_SHA: ${{ github.sha }} + VERCEL_GIT_COMMIT_REF: ${{ github.head_ref }} + VERCEL_GIT_PULL_REQUEST_ID: ${{ github.event.pull_request.number }} + - name: Set Deploy Preview Vercel Branch Alias + if: ${{ inputs.public-hostname != '' && inputs.production != 'true'}} + shell: bash + run: | + bunx vercel --token=${{ inputs.vercel-token }} -S ${{ inputs.vercel-scope }} alias set ${{ steps.deploy-preview.outputs.deployment_url }} ${{ inputs.public-hostname }} + echo ::notice::Vercel Alias URL https://${{ inputs.public-hostname }}/ + env: + VERCEL_ORG_ID: ${{ inputs.vercel-org-id }} + VERCEL_PROJECT_ID: ${{ inputs.vercel-project-id }} + # END Preview Environment + + # BEGIN Production Environment + - name: Setup Production Environment + if: ${{ inputs.production == 'true' }} + shell: bash + run: | + bunx vercel --token=${{ inputs.vercel-token }} -S ${{ inputs.vercel-scope }} pull --yes --environment=production + cp .vercel/.env.production.local .env.local env: VERCEL_ORG_ID: ${{ inputs.vercel-org-id }} VERCEL_PROJECT_ID: ${{ inputs.vercel-project-id }} - - name: Build Project Artifacts + - name: Build Production Project Artifacts + if: ${{ inputs.production == 'true' }} shell: bash - run: bunx vercel --token=${{ inputs.vercel-token }} build + run: bunx vercel --token=${{ inputs.vercel-token }} -S ${{ inputs.vercel-scope }} build --prod env: VERCEL_ORG_ID: ${{ inputs.vercel-org-id }} VERCEL_PROJECT_ID: ${{ inputs.vercel-project-id }} - - name: Deploy Project Artifacts to Vercel - id: deploy + - name: Deploy Production + if: ${{ inputs.production == 'true' }} + id: deploy-prod shell: bash run: | - if [[ ${{ inputs.production }} == "true" ]]; then - bunx vercel --token=${{ inputs.vercel-token }} deploy --prebuilt --prod > deployment-url.txt - else - bunx vercel --token=${{ inputs.vercel-token }} deploy --prebuilt > deployment-url.txt - fi + bunx vercel --token=${{ inputs.vercel-token }} -S ${{ inputs.vercel-scope }} deploy --prebuilt --prod > deployment-url.txt echo "deployment_url=$(cat deployment-url.txt)" >> $GITHUB_OUTPUT echo ::notice::Deployment URL: $(cat deployment-url.txt) env: VERCEL_ORG_ID: ${{ inputs.vercel-org-id }} VERCEL_PROJECT_ID: ${{ inputs.vercel-project-id }} - - name: Set Vercel Branch Alias - if: ${{ inputs.public-hostname != '' }} + VERCEL_GIT_COMMIT_SHA: ${{ github.sha }} + VERCEL_GIT_COMMIT_REF: ${{ github.head_ref }} + VERCEL_GIT_PULL_REQUEST_ID: ${{ github.event.pull_request.number }} + - name: Set Deploy Production Vercel Branch Alias + if: ${{ inputs.public-hostname != '' && inputs.production == 'true'}} shell: bash run: | - bunx vercel --token=${{ inputs.vercel-token }} -S 0xsend alias set ${{ steps.deploy.outputs.deployment_url }} ${{ inputs.public-hostname }} + bunx vercel --token=${{ inputs.vercel-token }} -S ${{ inputs.vercel-scope }} alias set ${{ steps.deploy-prod.outputs.deployment_url }} ${{ inputs.public-hostname }} echo ::notice::Vercel Alias URL https://${{ inputs.public-hostname }}/ env: VERCEL_ORG_ID: ${{ inputs.vercel-org-id }} VERCEL_PROJECT_ID: ${{ inputs.vercel-project-id }} + # END Production Environment diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aa386dbf4..d1c778ed6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,7 @@ on: pull_request: push: branches: [main, dev] + workflow_dispatch: concurrency: group: ${{ github.ref }} @@ -20,20 +21,38 @@ jobs: - name: Setup Environment uses: ./.github/actions/setup-env with: - install-foundry: false - install-bun: false + build-nextjs: true + env: + YARN_ENABLE_HARDENED_MODE: 0 + - name: Show github refs + run: | + echo github.ref=${{ github.ref }} + echo github.head_ref=${{ github.head_ref }} + + lint: + runs-on: ubuntu-latest + needs: [cache-deps] + env: + YARN_ENABLE_HARDENED_MODE: 0 + + steps: + - uses: actions/checkout@v4 + - name: Setup Environment + uses: ./.github/actions/setup-env env: YARN_ENABLE_HARDENED_MODE: 0 SKIP_YARN_POST_INSTALL: 1 + - name: Lint + run: yarn lint - ci: - # skip if on dev and main (but not PRS to dev or main) - if: ${{ !(contains(github.ref, 'dev') || contains(github.ref, 'main')) }} + unit-tests: + name: Unit Tests runs-on: ubuntu-latest needs: [cache-deps] env: ANVIL_MAINNET_FORK_URL: ${{ secrets.CI_ANVIL_MAINNET_FORK_URL }} ANVIL_BASE_FORK_URL: ${{ secrets.CI_ANVIL_BASE_FORK_URL }} + FOUNDRY_BASE_SEPOLIA_RPC_URL: ${{ secrets.CI_FOUNDRY_BASE_SEPOLIA_RPC_URL }} YARN_ENABLE_HARDENED_MODE: 0 steps: @@ -42,7 +61,7 @@ jobs: submodules: recursive # No idea how homebrew bundle broke with tilt depending on python so installing manually - name: Install Tilt - run: curl -fsSL https://raw.githubusercontent.com/tilt-dev/tilt/master/scripts/install.sh | bash + uses: ./.github/actions/install-tilt - name: Set up Homebrew id: set-up-homebrew uses: Homebrew/actions/setup-homebrew@master @@ -57,17 +76,63 @@ jobs: YARN_ENABLE_HARDENED_MODE: 0 with: yarn-install: false - - name: Tilt CI + - name: Load Next.js Build Cache + uses: actions/download-artifact@v4 + with: + name: nextjs-build + path: ${{ github.workspace }}/apps/next/.next + - name: Snaplet Login shell: bash - run: tilt ci unit-tests:tests - - name: All Changes Committed + run: bunx snaplet auth login ${{ secrets.CI_SNAPLET_AUTH_TOKEN }} + - name: Tilt CI shell: bash - run: git diff --exit-code + run: tilt ci unit-tests + # @todo anvil fixtures cause dirty repo, but only in github actions 😢 + # - name: All Changes Committed + # shell: bash + # run: git diff --exit-code + + playwright-tests: + name: Playwright Tests + runs-on: ubuntu-latest + needs: [cache-deps] + env: + ANVIL_MAINNET_FORK_URL: ${{ secrets.CI_ANVIL_MAINNET_FORK_URL }} + ANVIL_BASE_FORK_URL: ${{ secrets.CI_ANVIL_BASE_FORK_URL }} + YARN_ENABLE_HARDENED_MODE: 0 + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install Tilt + uses: ./.github/actions/install-tilt + - name: Set up Homebrew + id: set-up-homebrew + uses: Homebrew/actions/setup-homebrew@master + - name: Brew Bundle + id: brew-bundle + run: | + brew bundle + brew cleanup + - name: Setup Environment + uses: ./.github/actions/setup-env + env: + YARN_ENABLE_HARDENED_MODE: 0 + - name: Load Next.js Build Cache + uses: actions/download-artifact@v4 + with: + name: nextjs-build + path: ${{ github.workspace }}/apps/next/.next - name: Snaplet Login shell: bash run: bunx snaplet auth login ${{ secrets.CI_SNAPLET_AUTH_TOKEN }} - name: Install Playwright Dependencies run: yarn playwright playwright install --with-deps + # @todo anvil fixtures cause dirty repo, but only in github actions 😢 + # - name: All Changes Committed + # shell: bash + # run: git diff --exit-code # this is on purpose so we get github annotations - name: Playwright Tests id: playwright @@ -75,7 +140,7 @@ jobs: run: | # set debug logs if runner.debug is set if [ "${{runner.debug}}" == "1" ]; then - export DEBUG=test:*,app:* + export DEBUG="test:*,app:*,api:*,next:*" fi nohup tilt up playwright:deps & echo waiting for tilt to be ready @@ -83,8 +148,9 @@ jobs: curl -s http://localhost:10350 > /dev/null && break || echo "Attempt $i failed, trying again..." sleep 1 done + sleep 1 echo tilt is ready - tilt wait --timeout 60s --for=condition=Ready "uiresource/playwright:deps" + time tilt wait --timeout 5m --for=condition=Ready "uiresource/playwright:deps" yarn playwright test - name: Playwright Report uses: actions/upload-artifact@v4 @@ -96,7 +162,7 @@ jobs: vercel-deploy-preview: # **always** skip if on dev and main since it will be deployed with another workflow - if: ${{ !(contains(github.ref, 'dev') || contains(github.ref, 'main') || contains(github.head_ref, 'dev') || contains(github.head_ref, 'main')) }} + if: ${{ github.ref != 'refs/heads/dev' && github.ref != 'refs/heads/main' && github.head_ref != 'dev' && github.head_ref != 'main' }} runs-on: ubuntu-latest needs: [cache-deps] @@ -117,14 +183,97 @@ jobs: yarn-install: false env: YARN_ENABLE_HARDENED_MODE: 0 + - name: Load Next.js Build Cache + uses: actions/download-artifact@v4 + with: + name: nextjs-build + path: ${{ github.workspace }}/apps/next/.next + - name: Get Supabase Database Branch + if: github.base_ref == 'dev' + uses: 0xbigboss/supabase-branch-gh-action@v1 + id: supabase-branch + with: + supabase-access-token: ${{ secrets.SUPABASE_EXPERIMENTAL_ACCESS_TOKEN }} + supabase-project-id: ${{ secrets.STAGING_SUPABASE_PROJECT_ID }} + wait-for-migrations: false # Optional. Default is false. + timeout: 60 # Optional. Default is 60. + - name: Add SMS provider to Supabase branch + if: github.base_ref == 'dev' + uses: 0xbigboss/supabase-manager-script-gh-action@v1 + id: add-sms-provider + with: + supabase-access-token: ${{ secrets.SUPABASE_EXPERIMENTAL_ACCESS_TOKEN }} + script: | + const parentAuthConfig = await supabaseManager.projectsConfig.getV1AuthConfig({ + ref: process.env.SUPABASE_PARENT_PROJECT_ID, + }); + + core.info('Enabling Twilio verify external phone auth provider'); + + await supabaseManager.projectsConfig.updateV1AuthConfig({ + ref: process.env.SUPABASE_PROJECT_ID, + requestBody: { + external_phone_enabled: true, + sms_provider: parentAuthConfig.sms_provider, + sms_twilio_verify_account_sid: + parentAuthConfig.sms_twilio_verify_account_sid, + sms_twilio_verify_auth_token: parentAuthConfig.sms_twilio_verify_auth_token, + sms_twilio_verify_message_service_sid: + parentAuthConfig.sms_twilio_verify_message_service_sid, + }, + }); + + core.info('Done'); + + return "success"; + env: + SUPABASE_PROJECT_ID: ${{ steps.supabase-branch.outputs.project_ref }} + SUPABASE_PARENT_PROJECT_ID: ${{ steps.supabase-branch.outputs.parent_project_ref }} - name: Extract branch name id: extract-branch uses: ./.github/actions/extract-branch - name: Set Public Hostname id: public-hostname run: echo "public-hostname=sendapp-${{steps.extract-branch.outputs.branch}}-0xsend.vercel.app" >> $GITHUB_OUTPUT - - name: Vercel Deploy - id: vercel-deploy + - name: Vercel Deploy Preview with Supabase Branch + if: github.base_ref == 'dev' + id: vercel-deploy-with-branch + uses: ./.github/actions/vercel-deploy + with: + vercel-token: ${{ secrets.VERCEL_TOKEN }} + public-hostname: ${{ steps.public-hostname.outputs.public-hostname }} + deploy-preview-extra-args: >- + -e SUPABASE_DB_URL="postgresql://${{steps.supabase-branch.outputs.db_user}}.${{steps.supabase-branch.outputs.project_ref}}:${{steps.supabase-branch.outputs.db_pass}}@fly-0-iad.pooler.supabase.com:${{steps.supabase-branch.outputs.db_port}}/postgres" + -e SUPABASE_JWT_SECRET="${{steps.supabase-branch.outputs.jwt_secret}}" + -e SUPABASE_SERVICE_ROLE="${{ steps.supabase-branch.outputs.service_role_key }}" + -e NEXT_PUBLIC_SUPABASE_URL="https://${{ steps.supabase-branch.outputs.project_ref }}.supabase.co" + -e NEXT_PUBLIC_SUPABASE_PROJECT_ID="${{steps.supabase-branch.outputs.project_ref}}" + -e NEXT_PUBLIC_SUPABASE_GRAPHQL_URL="${{steps.supabase-branch.outputs.graphql_url}}" + -e NEXT_PUBLIC_BASE_CHAIN_ID="84532" + -e NEXT_PUBLIC_MAINNET_CHAIN_ID="11155111" + -e NEXT_PUBLIC_BASE_RPC_URL="${{ secrets.BASE_SEPOLIA_RPC_URL }}" + -e NEXT_PUBLIC_MAINNET_RPC_URL="https://ethereum-sepolia-rpc.publicnode.com" + -e NEXT_PUBLIC_SUPABASE_ANON_KEY="${{ steps.supabase-branch.outputs.anon_key }}" + env: + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + VERCEL_GIT_COMMIT_SHA: ${{ github.sha }} + VERCEL_GIT_COMMIT_REF: ${{ github.head_ref }} + VERCEL_GIT_PULL_REQUEST_ID: ${{ github.event.pull_request.number }} + SUPABASE_DB_URL: postgresql://${{steps.supabase-branch.outputs.db_user}}.${{steps.supabase-branch.outputs.project_ref}}:${{steps.supabase-branch.outputs.db_pass}}@fly-0-iad.pooler.supabase.com:${{steps.supabase-branch.outputs.db_port}}/postgres + SUPABASE_JWT_SECRET: ${{steps.supabase-branch.outputs.jwt_secret}} + SUPABASE_SERVICE_ROLE: ${{ steps.supabase-branch.outputs.service_role_key }} + NEXT_PUBLIC_SUPABASE_URL: https://${{ steps.supabase-branch.outputs.project_ref }}.supabase.co + NEXT_PUBLIC_SUPABASE_PROJECT_ID: ${{steps.supabase-branch.outputs.project_ref}} + NEXT_PUBLIC_SUPABASE_GRAPHQL_URL: ${{steps.supabase-branch.outputs.graphql_url}} + NEXT_PUBLIC_BASE_CHAIN_ID: 84532 + NEXT_PUBLIC_MAINNET_CHAIN_ID: 11155111 + NEXT_PUBLIC_BASE_RPC_URL: ${{ secrets.BASE_SEPOLIA_RPC_URL }} + NEXT_PUBLIC_MAINNET_RPC_URL: https://ethereum-sepolia-rpc.publicnode.com + NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ steps.supabase-branch.outputs.anon_key }} + - name: Vercel Deploy Preview + if: github.base_ref != 'dev' + id: vercel-deploy-preview uses: ./.github/actions/vercel-deploy with: vercel-token: ${{ secrets.VERCEL_TOKEN }} @@ -132,12 +281,29 @@ jobs: env: VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + VERCEL_GIT_COMMIT_SHA: ${{ github.sha }} + VERCEL_GIT_COMMIT_REF: ${{ github.head_ref }} + VERCEL_GIT_PULL_REQUEST_ID: ${{ github.event.pull_request.number }} + + - uses: mshick/add-pr-comment@v2 + if: github.base_ref == 'dev' + with: + message-id: vercel-preview-url + refresh-message-position: true + message: | + Vercel Unique URL: [${{ steps.vercel-deploy-with-branch.outputs.deployment-url }}](${{ steps.vercel-deploy-with-branch.outputs.deployment-url }}) + Vercel Preview URL: [${{ steps.public-hostname.outputs.public-hostname }}](https://${{ steps.public-hostname.outputs.public-hostname }}/) + Last Commit: ${{ github.event.pull_request.head.sha }} + - uses: mshick/add-pr-comment@v2 + if: github.base_ref != 'dev' with: message-id: vercel-preview-url + refresh-message-position: true message: | - Vercel Unique URL: [${{ steps.vercel-deploy.outputs.deployment-url }}](${{ steps.vercel-deploy.outputs.deployment-url }}) + Vercel Unique URL: [${{ steps.vercel-deploy-preview.outputs.deployment-url }}](${{ steps.vercel-deploy-preview.outputs.deployment-url }}) Vercel Preview URL: [${{ steps.public-hostname.outputs.public-hostname }}](https://${{ steps.public-hostname.outputs.public-hostname }}/) + Last Commit: ${{ github.event.pull_request.head.sha }} deploy-staging: if: github.ref == 'refs/heads/dev' @@ -150,3 +316,17 @@ jobs: SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }} STAGING_SUPABASE_PROJECT_ID: ${{ secrets.STAGING_SUPABASE_PROJECT_ID }} STAGING_SUPABASE_DB_PASSWORD: ${{ secrets.STAGING_SUPABASE_DB_PASSWORD }} + CI_QA_NOTIFICATION_URL: ${{ secrets.CI_QA_NOTIFICATION_URL }} + + deploy-production: + if: github.ref == 'refs/heads/main' + needs: [cache-deps] + uses: ./.github/workflows/deploy-production.yml + secrets: + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }} + PRODUCTION_SUPABASE_PROJECT_ID: ${{ secrets.PRODUCTION_SUPABASE_PROJECT_ID }} + PRODUCTION_SUPABASE_DB_PASSWORD: ${{ secrets.PRODUCTION_SUPABASE_DB_PASSWORD }} + CI_QA_NOTIFICATION_URL: ${{ secrets.CI_QA_NOTIFICATION_URL }} diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml new file mode 100644 index 000000000..138875974 --- /dev/null +++ b/.github/workflows/deploy-production.yml @@ -0,0 +1,49 @@ +name: Deploy Production + +on: + workflow_dispatch: + workflow_call: + secrets: + VERCEL_TOKEN: + description: 'Vercel token' + required: true + VERCEL_PROJECT_ID: + description: 'Vercel project id' + required: true + VERCEL_ORG_ID: + description: 'Vercel org id' + required: true + SUPABASE_ACCESS_TOKEN: + description: 'Supabase access token' + required: true + PRODUCTION_SUPABASE_PROJECT_ID: + description: 'Production Supabase project id' + required: true + PRODUCTION_SUPABASE_DB_PASSWORD: + description: 'Production Supabase db password' + required: true + CI_QA_NOTIFICATION_URL: + description: 'QA notification url' + required: true + +concurrency: + group: ${{ github.workflow }} + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Deploy + uses: ./.github/actions/deploy + with: + vercel-token: ${{ secrets.VERCEL_TOKEN }} + vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }} + vercel-org-id: ${{ secrets.VERCEL_ORG_ID }} + production: true + supabase-project-id: ${{ secrets.PRODUCTION_SUPABASE_PROJECT_ID }} + supabase-access-token: ${{ secrets.SUPABASE_ACCESS_TOKEN }} + supabase-db-password: ${{ secrets.PRODUCTION_SUPABASE_DB_PASSWORD }} + qa-notification-url: ${{ secrets.CI_QA_NOTIFICATION_URL }} diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml index 81d68abb8..2df0f7c91 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy-staging.yml @@ -22,6 +22,9 @@ on: STAGING_SUPABASE_DB_PASSWORD: description: 'Staging Supabase db password' required: true + CI_QA_NOTIFICATION_URL: + description: 'QA notification url' + required: true concurrency: group: ${{ github.workflow }} @@ -43,3 +46,4 @@ jobs: supabase-project-id: ${{ secrets.STAGING_SUPABASE_PROJECT_ID }} supabase-access-token: ${{ secrets.SUPABASE_ACCESS_TOKEN }} supabase-db-password: ${{ secrets.STAGING_SUPABASE_DB_PASSWORD }} + qa-notification-url: ${{ secrets.CI_QA_NOTIFICATION_URL }} diff --git a/.snaplet/snaplet-client.d.ts b/.snaplet/snaplet-client.d.ts index e7511ac0d..2036db34c 100644 --- a/.snaplet/snaplet-client.d.ts +++ b/.snaplet/snaplet-client.d.ts @@ -14,19 +14,6 @@ type Inflection = { oppositeBaseNameMap?: Record; }; type Override = { - _http_response?: { - name?: string; - fields?: { - id?: string; - status_code?: string; - content_type?: string; - headers?: string; - content?: string; - timed_out?: string; - error_msg?: string; - created?: string; - }; - } buckets?: { name?: string; fields?: { @@ -113,6 +100,8 @@ type Override = { hodler_min_balance?: string; created_at?: string; updated_at?: string; + snapshot_block_num?: string; + chain_id?: string; distribution_shares?: string; distribution_verification_values?: string; distribution_verifications?: string; @@ -128,17 +117,6 @@ type Override = { request_id?: string; }; } - http_request_queue?: { - name?: string; - fields?: { - id?: string; - method?: string; - url?: string; - headers?: string; - body?: string; - timeout_milliseconds?: string; - }; - } storage_migrations?: { name?: string; fields?: { @@ -181,6 +159,7 @@ type Override = { about?: string; referral_code?: string; is_public?: string; + send_id?: string; users?: string; referrals_referrals_referred_idToprofiles?: string; referrals_referrals_referrer_idToprofiles?: string; @@ -233,6 +212,45 @@ type Override = { webauthn_credentials?: string; }; } + send_account_deployed?: { + name?: string; + fields?: { + id?: string; + chain_id?: string; + log_addr?: string; + block_time?: string; + tx_hash?: string; + user_op_hash?: string; + sender?: string; + factory?: string; + paymaster?: string; + ig_name?: string; + src_name?: string; + block_num?: string; + tx_idx?: string; + log_idx?: string; + abi_idx?: string; + }; + } + send_account_transfers?: { + name?: string; + fields?: { + id?: string; + chain_id?: string; + log_addr?: string; + block_time?: string; + tx_hash?: string; + f?: string; + t?: string; + v?: string; + ig_name?: string; + src_name?: string; + block_num?: string; + tx_idx?: string; + log_idx?: string; + abi_idx?: string; + }; + } send_accounts?: { name?: string; fields?: { @@ -248,6 +266,33 @@ type Override = { send_account_credentials?: string; }; } + send_liquidity_pools?: { + name?: string; + fields?: { + id?: string; + address?: string; + chain_id?: string; + }; + } + send_token_transfers?: { + name?: string; + fields?: { + id?: string; + chain_id?: string; + log_addr?: string; + block_time?: string; + tx_hash?: string; + f?: string; + t?: string; + v?: string; + ig_name?: string; + src_name?: string; + block_num?: string; + tx_idx?: string; + log_idx?: string; + abi_idx?: string; + }; + } send_transfer_logs?: { name?: string; fields?: { @@ -380,12 +425,6 @@ interface FingerprintNumberField { } } export interface Fingerprint { - HttpResponses?: { - id?: FingerprintNumberField; - statusCode?: FingerprintNumberField; - headers?: FingerprintJsonField; - created?: FingerprintDateField; - } buckets?: { createdAt?: FingerprintDateField; updatedAt?: FingerprintDateField; @@ -439,6 +478,8 @@ export interface Fingerprint { hodlerMinBalance?: FingerprintNumberField; createdAt?: FingerprintDateField; updatedAt?: FingerprintDateField; + snapshotBlockNum?: FingerprintNumberField; + chainId?: FingerprintNumberField; distributionShares?: FingerprintRelationField; distributionVerificationValues?: FingerprintRelationField; distributionVerifications?: FingerprintRelationField; @@ -449,11 +490,6 @@ export interface Fingerprint { createdAt?: FingerprintDateField; requestId?: FingerprintNumberField; } - httpRequestQueues?: { - id?: FingerprintNumberField; - headers?: FingerprintJsonField; - timeoutMilliseconds?: FingerprintNumberField; - } storageMigrations?: { id?: FingerprintNumberField; executedAt?: FingerprintDateField; @@ -469,6 +505,7 @@ export interface Fingerprint { bucket?: FingerprintRelationField; } profiles?: { + sendId?: FingerprintNumberField; i?: FingerprintRelationField; referralsByReferredId?: FingerprintRelationField; referralsByReferrerId?: FingerprintRelationField; @@ -496,6 +533,25 @@ export interface Fingerprint { account?: FingerprintRelationField; credential?: FingerprintRelationField; } + sendAccountDeployeds?: { + id?: FingerprintNumberField; + chainId?: FingerprintNumberField; + blockTime?: FingerprintNumberField; + blockNum?: FingerprintNumberField; + txIdx?: FingerprintNumberField; + logIdx?: FingerprintNumberField; + abiIdx?: FingerprintNumberField; + } + sendAccountTransfers?: { + id?: FingerprintNumberField; + chainId?: FingerprintNumberField; + blockTime?: FingerprintNumberField; + v?: FingerprintNumberField; + blockNum?: FingerprintNumberField; + txIdx?: FingerprintNumberField; + logIdx?: FingerprintNumberField; + abiIdx?: FingerprintNumberField; + } sendAccounts?: { chainId?: FingerprintNumberField; createdAt?: FingerprintDateField; @@ -504,6 +560,20 @@ export interface Fingerprint { user?: FingerprintRelationField; sendAccountCredentialsByAccountId?: FingerprintRelationField; } + sendLiquidityPools?: { + id?: FingerprintNumberField; + chainId?: FingerprintNumberField; + } + sendTokenTransfers?: { + id?: FingerprintNumberField; + chainId?: FingerprintNumberField; + blockTime?: FingerprintNumberField; + v?: FingerprintNumberField; + blockNum?: FingerprintNumberField; + txIdx?: FingerprintNumberField; + logIdx?: FingerprintNumberField; + abiIdx?: FingerprintNumberField; + } sendTransferLogs?: { value?: FingerprintNumberField; blockNumber?: FingerprintNumberField; diff --git a/.snaplet/snaplet.d.ts b/.snaplet/snaplet.d.ts index cba86e31e..e76e4b794 100644 --- a/.snaplet/snaplet.d.ts +++ b/.snaplet/snaplet.d.ts @@ -93,6 +93,8 @@ interface Table_public_distributions { hodler_min_balance: number; created_at: string; updated_at: string; + snapshot_block_num: number | null; + chain_id: number; } interface Table_pgtle_feature_info { feature: Enum_pgtle_pg_tle_features; @@ -129,13 +131,23 @@ interface Table_net_http_request_queue { timeout_milliseconds: number; } interface Table_auth_identities { - id: string; + provider_id: string; user_id: string; identity_data: Json; provider: string; last_sign_in_at: string | null; created_at: string | null; updated_at: string | null; + id: string; +} +interface Table_shovel_ig_updates { + name: string; + src_name: string; + backfill: boolean | null; + num: number; + latency: string | null; + nrows: number | null; + stop: number | null; } interface Table_auth_instances { id: string; @@ -144,6 +156,10 @@ interface Table_auth_instances { created_at: string | null; updated_at: string | null; } +interface Table_shovel_integrations { + name: string | null; + conf: Json | null; +} interface Table_pgsodium_key { id: string; status: Enum_pgsodium_key_status | null; @@ -213,6 +229,7 @@ interface Table_public_profiles { about: string | null; referral_code: string | null; is_public: boolean | null; + send_id: number; } interface Table_public_receipts { hash: string; @@ -281,6 +298,39 @@ interface Table_public_send_account_credentials { key_slot: number; created_at: string | null; } +interface Table_public_send_account_deployed { + id: number; + chain_id: number | null; + log_addr: string | null; + block_time: number | null; + tx_hash: string | null; + user_op_hash: string | null; + sender: string | null; + factory: string | null; + paymaster: string | null; + ig_name: string | null; + src_name: string | null; + block_num: number | null; + tx_idx: number | null; + log_idx: number | null; + abi_idx: number | null; +} +interface Table_public_send_account_transfers { + id: number; + chain_id: number | null; + log_addr: string | null; + block_time: number | null; + tx_hash: string | null; + f: string | null; + t: string | null; + v: number | null; + ig_name: string | null; + src_name: string | null; + block_num: number | null; + tx_idx: number | null; + log_idx: number | null; + abi_idx: number | null; +} interface Table_public_send_accounts { id: string; user_id: string; @@ -291,6 +341,27 @@ interface Table_public_send_accounts { updated_at: string; deleted_at: string | null; } +interface Table_public_send_liquidity_pools { + id: number; + address: string; + chain_id: number; +} +interface Table_public_send_token_transfers { + id: number; + chain_id: number | null; + log_addr: string | null; + block_time: number | null; + tx_hash: string | null; + f: string | null; + t: string | null; + v: number | null; + ig_name: string | null; + src_name: string | null; + block_num: number | null; + tx_idx: number | null; + log_idx: number | null; + abi_idx: number | null; +} interface Table_public_send_transfer_logs { from: string; to: string; @@ -310,6 +381,15 @@ interface Table_auth_sessions { factor_id: string | null; aal: Enum_auth_aal_level | null; not_after: string | null; + refreshed_at: string | null; + user_agent: string | null; + ip: string | null; + tag: string | null; +} +interface Table_shovel_sources { + name: string | null; + chain_id: number | null; + url: string | null; } interface Table_auth_sso_domains { id: string; @@ -339,6 +419,20 @@ interface Table_public_tags { user_id: string; created_at: string; } +interface Table_shovel_task_updates { + num: number | null; + hash: string | null; + insert_at: string | null; + src_hash: string | null; + src_num: number | null; + nblocks: number | null; + nrows: number | null; + latency: string | null; + src_name: string | null; + stop: number | null; + chain_id: number | null; + ig_name: string | null; +} interface Table_auth_users { instance_id: string | null; id: string; @@ -446,7 +540,11 @@ interface Schema_public { receipts: Table_public_receipts; referrals: Table_public_referrals; send_account_credentials: Table_public_send_account_credentials; + send_account_deployed: Table_public_send_account_deployed; + send_account_transfers: Table_public_send_account_transfers; send_accounts: Table_public_send_accounts; + send_liquidity_pools: Table_public_send_liquidity_pools; + send_token_transfers: Table_public_send_token_transfers; send_transfer_logs: Table_public_send_transfer_logs; tag_receipts: Table_public_tag_receipts; tag_reservations: Table_public_tag_reservations; @@ -455,6 +553,12 @@ interface Schema_public { } interface Schema_realtime { +} +interface Schema_shovel { + ig_updates: Table_shovel_ig_updates; + integrations: Table_shovel_integrations; + sources: Table_shovel_sources; + task_updates: Table_shovel_task_updates; } interface Schema_storage { buckets: Table_storage_buckets; @@ -485,6 +589,7 @@ interface Database { pgtle: Schema_pgtle; public: Schema_public; realtime: Schema_realtime; + shovel: Schema_shovel; storage: Schema_storage; supabase_functions: Schema_supabase_functions; supabase_migrations: Schema_supabase_migrations; diff --git a/.solhint.json b/.solhint.json index d7c3de989..380f5fb80 100644 --- a/.solhint.json +++ b/.solhint.json @@ -1,3 +1,6 @@ { - "extends": "solhint:default" + "extends": "solhint:default", + "rules": { + "max-line-length": "off" + } } diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 6f60046b8..82a374e61 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -6,7 +6,6 @@ "tilt-dev.tiltfile", "yoavbls.pretty-ts-errors", "dbaeumer.vscode-eslint", - "usernamehw.errorlens", - "bradymholt.pgformatter" + "usernamehw.errorlens" ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 1d9404b3f..670f9d75b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -24,7 +24,7 @@ "editor.defaultFormatter": "biomejs.biome" }, "[jsonc]": { - "editor.defaultFormatter": "biomejs.biome" + "editor.defaultFormatter": "esbenp.prettier-vscode" }, "editor.codeActionsOnSave": { "quickfix.biome": "explicit", @@ -45,11 +45,5 @@ "@openzeppelin/contracts/=packages/contracts/lib/openzeppelin-contracts/contracts/", "openzeppelin-contracts-upgradeable/=packages/contracts/lib/openzeppelin-contracts-upgradeable/", "p256-verifier/=packages/contracts/lib/p256-verifier/src/" - ], - "[sql]": { - "editor.defaultFormatter": "bradymholt.pgformatter" - }, - "pgFormatter.keywordCase": "lowercase", - "pgFormatter.wrapLimit": 100, - "pgFormatter.keepNewline": true + ] } diff --git a/Brewfile b/Brewfile index e17276ce6..18b640809 100644 --- a/Brewfile +++ b/Brewfile @@ -12,3 +12,4 @@ brew "jq" unless system "jq --version" brew "yj" unless system "yj -v" brew "tilt" unless system "tilt version" brew "caddy" unless system "caddy version" +brew "nss" unless system "nss-policy-check" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3d3e9126a..dcee2951c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,11 +1,11 @@ + + # Contribution Guide ## Preface Send Stack uses bleeding-edge web development technology to deliver great DX to our contributors, quick iteration cycles, and clean code. -**Send Stack** - - **Typescript** (strict mode) - **Bun** Package Manager - **Yarn** workspaces w/ Yarn berry @@ -22,6 +22,7 @@ Send Stack uses bleeding-edge web development technology to deliver great DX to Here is a quick peek at the send stack. Quickly jump to any of the submodules by clicking the links below.
+
 .
 ├── apps
 │   ├── distributor
@@ -39,7 +40,7 @@ Here is a quick peek at the send stack. Quickly jump to any of the submodules by
 │   ├── wagmi
 │   └── webauthn-authenticator
 └── supabase
- 
+
 
@@ -48,7 +49,7 @@ Here is a quick peek at the send stack. Quickly jump to any of the submodules by Here are some things to keep in mind about thee SEND philosophy when contributing +
-

Prerequisites

+When cloning the repo, you will need to initialize the submodules: + ```console -git clone https://github.com/0xsend/sendapp.git && cd sendapp +git clone --recurse-submodules https://github.com/0xsend/sendapp.git && cd sendapp ``` +If you missed the `--recurse-submodules` flag, you can initialize them manually: + +```console +git submodule deinit --force . +git submodule update --init --recursive +``` + +### Tools + You'll need a basic understanding of JS tooling Required JS Runtime: [Node >= 20.9.0](https://nodejs.org/en/download) -#### [yarn package manager](https://yarnpkg.com/) +#### [Yarn Package Manager](https://yarnpkg.com/) + +See [yarn package manager](https://yarnpkg.com/). We are using Yarn 4 with workspaces. ```console corepack enable @@ -108,7 +121,8 @@ curl -fsSL https://bun.sh/install | bash #### Brew Bundle -Many other dependencies are installed via [Homebrew](https://brew.sh/). To install all dependencies, run from the project root: +Many other dependencies are installed via [Homebrew](https://brew.sh/). To install all dependencies, run from the +project root: ```console brew bundle @@ -137,6 +151,20 @@ tilt up This command will start all the services defined in the [Tiltfile](/Tiltfile), building and deploying your application in a local development environment. +##### Efficient Tilt Usage + +`tilt up` will start a local Postgres database, Supabase, local Ethereum node, and local Base node. It also starts the unit tests for the application. + +To save some resources on your local machine, you can limit the amount of resources used by Tilt by specifying them on the command line or disabling them in the [Tilt UI](http://localhost:10350). + +This command for example will only start the Next.js web app and it's dependencies: + +```console +tilt up next:web +``` + +You can always re-enable the disabled resources by re-running the `tilt up` command or manually enabling them in the [Tilt UI](http://localhost:10350). + #### 2. Monitoring and Logs You can monitor the build process and access logs directly through the Tilt UI. Simply navigate to `http://localhost:10350` in your web browser to view the status of your services. @@ -149,6 +177,23 @@ With Tilt, you can make changes to your codebase, and Tilt will automatically de Once you're done developing, you can shut down all services by pressing `Ctrl+C` in the terminal where you ran `tilt up`. +It will leave somethings running in the background. To stop all services, run `tilt down`. + +```console +❯ tilt down +Loading Tiltfile at: /Users/bigboss/src/0xsend/sendapp/Tiltfile +Loading environment from .env +Loading environment from .env.local +local: sh -c "yarn supabase stop --no-backup\n # can be removed once supabase stop --no-backup is fixed\n docker volume ls --filter label=com.supabase.cli.project=send | awk 'NR>1 {print $2}' | xargs -I {} docker volume rm {}" + → Stopping containers... + → Stopped supabase local development setup. + → Local data are backed up to docker volume. Use docker to show them: docker volume ls --filter label=com.supabase.cli.project=send + → supabase_storage_send +local: yarn clean + → Done in 0s 663ms +Successfully loaded Tiltfile (3.632166166s) +``` + By leveraging Tilt, you can focus more on coding and less on the setup, significantly improving your development experience with the Send Stack.
diff --git a/README.md b/README.md index db5721ddc..51e969bcb 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,10 @@ ![GitHub forks](https://img.shields.io/github/forks/0xsend/sendapp?style=social) ![GitHub top language](https://img.shields.io/github/languages/top/0xsend/sendapp?color=yellow) - # Send App for Ethereum A monorepo based on [Tamagui](https://tamagui.dev) for a simple app to send ETH and ERC20 tokens. + +## Contributing + +See the [CONTRIBUTING.md](CONTRIBUTING.md) file for details. diff --git a/Tiltfile b/Tiltfile index 873913828..a7be55b3c 100644 --- a/Tiltfile +++ b/Tiltfile @@ -60,18 +60,18 @@ contract_files = files_matching( ) local_resource( - "contracts:build", - "yarn contracts build --sizes", + name = "contracts:build", allow_parallel = True, + cmd = "yarn contracts build --sizes", labels = labels, resource_deps = ["yarn:install"], deps = contract_files, ) local_resource( - "wagmi:generate", - "yarn wagmi build", + name = "wagmi:generate", allow_parallel = True, + cmd = "yarn wagmi generate", labels = labels, resource_deps = [ "yarn:install", @@ -90,9 +90,9 @@ local_resource( ) local_resource( - "supabase:generate", - "yarn supabase g", + name = "supabase:generate", allow_parallel = True, + cmd = "yarn supabase g", labels = labels, resource_deps = [ "yarn:install", @@ -105,9 +105,9 @@ local_resource( ) local_resource( - "snaplet:generate", - "bunx snaplet generate", + name = "snaplet:generate", allow_parallel = True, + cmd = "bunx snaplet generate", labels = labels, resource_deps = [ "yarn:install", @@ -132,9 +132,9 @@ ui_files = files_matching( ) local_resource( - "ui:build", - "yarn workspace @my/ui build", + name = "ui:build", allow_parallel = True, + cmd = "yarn workspace @my/ui build", labels = labels, resource_deps = [ "yarn:install", @@ -143,9 +143,9 @@ local_resource( ) local_resource( - "ui:generate-theme", - "yarn workspace @my/ui generate-theme", + name = "ui:generate-theme", allow_parallel = True, + cmd = "yarn workspace @my/ui generate-theme", labels = labels, resource_deps = [ "yarn:install", @@ -154,9 +154,9 @@ local_resource( ) local_resource( - "daimo-expo-passkeys:build", - "yarn workspace @daimo/expo-passkeys build", + name = "daimo-expo-passkeys:build", allow_parallel = True, + cmd = "yarn workspace @daimo/expo-passkeys build", labels = labels, resource_deps = [ "yarn:install", @@ -176,9 +176,9 @@ local_resource( ) local_resource( - "webauthn-authenticator:build", - "yarn workspace @0xsend/webauthn-authenticator build", + name = "webauthn-authenticator:build", allow_parallel = True, + cmd = "yarn workspace @0xsend/webauthn-authenticator build", labels = labels, resource_deps = ["yarn:install"], deps = @@ -188,6 +188,51 @@ local_resource( ), ) +local_resource( + name = "shovel:generate-config", + allow_parallel = True, + cmd = "yarn workspace shovel generate", + labels = labels, + resource_deps = [ + "yarn:install", + "wagmi:generate", + ], + deps = files_matching( + os.path.join("packages", "shovel"), + lambda f: f.endswith(".ts"), + ), +) + +local_resource( + name = "shovel:test", + allow_parallel = True, + auto_init = not CI, + cmd = "yarn workspace shovel test", + labels = labels, + resource_deps = [ + "yarn:install", + "shovel:generate-config", + ], + trigger_mode = CI and TRIGGER_MODE_MANUAL or TRIGGER_MODE_AUTO, + deps = files_matching( + os.path.join("packages", "shovel", "etc"), + lambda f: f.endswith(".json"), + ), +) + +cmd_button( + name = "shovel:update-config", + argv = [ + "/bin/sh", + "-c", + "yarn workspace shovel test --update-snapshots && yarn workspace shovel generate", + ], + icon_name = "restart_alt", + location = location.RESOURCE, + resource = "shovel:test", + text = "shovel update-snapshot", +) + # INFRA labels = ["infra"] @@ -214,7 +259,12 @@ local_resource( ) if config.tilt_subcommand == "down": - local("yarn supabase stop --no-backup") + local(""" + yarn supabase stop --no-backup + # can be removed once supabase stop --no-backup is fixed + docker volume ls --filter label=com.supabase.cli.project=send | awk 'NR>1 {print $2}' | xargs -I {} docker volume rm {} + """) + local("yarn clean") cmd_button( "supabase:db reset", @@ -236,7 +286,7 @@ cmd_button( "-c", "yarn snaplet:seed", ], - icon_name = "delete_forever", + icon_name = "compost", location = location.NAV, resource = "supabase", text = "snaplet seed", @@ -276,18 +326,20 @@ local_resource( "--rpc-url=127.0.0.1:8545", ], ), - period_secs = 15, + initial_delay_secs = 1, + period_secs = 2, timeout_secs = 5, ), - serve_cmd = [ + serve_cmd = [cmd for cmd in [ "anvil", "--host=0.0.0.0", "--port=8545", - "--chain-id=1337", + "--chain-id=" + os.getenv("NEXT_PUBLIC_MAINNET_CHAIN_ID", "1337"), "--fork-url=" + os.getenv("ANVIL_MAINNET_FORK_URL", "https://eth-pokt.nodies.app"), "--fork-block-number=" + mainnet_fork_block_number, "--block-time=" + os.getenv("ANVIL_BLOCK_TIME", "5"), - ], + os.getenv("ANVIL_MAINNET_EXTRA_ARGS", "--silent"), + ] if cmd], ) local_resource( @@ -340,18 +392,20 @@ local_resource( "--rpc-url=127.0.0.1:8546", ], ), - period_secs = 15, + initial_delay_secs = 1, + period_secs = 2, timeout_secs = 5, ), - serve_cmd = [ + serve_cmd = [cmd for cmd in [ "anvil", "--host=0.0.0.0", "--port=8546", - "--chain-id=845337", + "--chain-id=" + os.getenv("NEXT_PUBLIC_BASE_CHAIN_ID", "845337"), "--fork-url=" + os.getenv("ANVIL_BASE_FORK_URL", "https://base-pokt.nodies.app"), "--fork-block-number=" + base_fork_block_number, "--block-time=" + os.getenv("ANVIL_BASE_BLOCK_TIME", "2"), - ], + os.getenv("ANVIL_BASE_EXTRA_ARGS", "--silent"), + ] if cmd], ) local_resource( @@ -367,7 +421,32 @@ local_resource( trigger_mode = TRIGGER_MODE_MANUAL, ) -# TODO: decide if we will use silius bundler or not +local_resource( + "anvil:anvil-add-send-merkle-drop-fixtures", + "yarn contracts dev:anvil-add-send-merkle-drop-fixtures", + labels = labels, + resource_deps = [ + "yarn:install", + "anvil:mainnet", + "anvil:base", + "contracts:build", + ], + trigger_mode = TRIGGER_MODE_MANUAL, +) + +local_resource( + "anvil:anvil-add-token-paymaster-fixtures", + "yarn contracts dev:anvil-add-token-paymaster-fixtures", + labels = labels, + resource_deps = [ + "yarn:install", + "anvil:mainnet", + "anvil:base", + "contracts:build", + ], + trigger_mode = TRIGGER_MODE_MANUAL, +) + local_resource( "aa_bundler:base", allow_parallel = True, @@ -377,22 +456,20 @@ local_resource( path = "/", port = 3030, ), - period_secs = 15, - timeout_secs = 5, ), resource_deps = [ "yarn:install", "anvil:base", ], serve_cmd = """ - docker ps -a | grep aa-bundler | awk '{print $1}' | xargs docker rm -f + docker ps -a | grep aa-bundler | awk '{{print $1}}' | xargs docker rm -f docker run --rm \ --name aa-bundler \ --add-host=host.docker.internal:host-gateway \ -p 3030:3030 \ -v ./keys/0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266:/app/keys/0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 \ -v ./etc/aa-bundler:/app/etc/aa-bundler \ - -e "DEBUG=aa*" \ + -e "DEBUG={bundler_debug}" \ -e "DEBUG_COLORS=true" \ docker.io/0xbigboss/bundler:0.7.0 \ --port 3030 \ @@ -402,31 +479,55 @@ local_resource( --entryPoint 0x0000000071727De22E5E9d8BAf0edAc6f37da032 \ --beneficiary 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 \ --unsafe - """, +""".format( + bundler_debug = os.getenv("BUNDLER_DEBUG", "aa.rpc"), + ), ) -# TODO: decide if we will use silius bundler or not -# local_resource( -# "silius:base", -# allow_parallel = True, -# labels = labels, -# readiness_probe = probe( -# exec = exec_action( -# command = [ -# "cast", -# "bn", -# "--rpc-url=127.0.0.1:3030", -# ], -# ), -# period_secs = 15, -# timeout_secs = 5, -# ), -# resource_deps = [ -# "yarn:install", -# "anvil:base", -# ], -# serve_cmd = "docker run --add-host=host.docker.internal:host-gateway -p 3030:3030 -v ./keys/0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266:/data/silius/0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 -v ./var/silius/db:/data/silius/db ghcr.io/silius-rs/silius:latest node --uopool-mode unsafe --eth-client-address http://host.docker.internal:8546 --datadir data/silius --mnemonic-file data/silius/0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 --beneficiary 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 --entry-points 0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789 --http --http.addr 0.0.0.0 --http.port 3030 --http.api eth,debug,web3 --ws --ws.addr 0.0.0.0 --ws.port 3001 --ws.api eth,debug,web3 --eth-client-proxy-address http://host.docker.internal:8546", -# ) +local_resource( + "shovel", + allow_parallel = True, + auto_init = False, # shovel eats a lot of RPCs, so we don't want it to start automatically + labels = labels, + links = ["http://localhost:8383/"], + readiness_probe = probe( + http_get = http_get_action( + path = "/diag", + port = 8383, + ), + ), + resource_deps = [ + "yarn:install", + "anvil:base", + "supabase:test", + "shovel:generate-config", + ], + serve_cmd = """ + docker ps -a | grep shovel | awk '{{print $1}}' | xargs docker rm -f + docker pull docker.io/indexsupply/shovel:latest || true + docker run --rm \ + --name shovel \ + --add-host=host.docker.internal:host-gateway \ + -p 8383:80 \ + --env DATABASE_URL=postgresql://postgres:postgres@host.docker.internal:54322/postgres \ + --env BASE_NAME=base \ + --env BASE_RPC_URL=http://host.docker.internal:8546 \ + --env BASE_CHAIN_ID={chain_id} \ + --env BASE_BLOCK_START={bn} \ + --env DASHBOARD_ROOT_PASSWORD=shoveladmin \ + -v ./packages/shovel/etc:/etc/shovel \ + --entrypoint /usr/local/bin/shovel \ + -w /usr/local/bin \ + docker.io/indexsupply/shovel -l :80 -config /etc/shovel/config.json + """.format( + bn = base_fork_block_number, + chain_id = os.getenv("NEXT_PUBLIC_BASE_CHAIN_ID", "845337"), + ), + trigger_mode = TRIGGER_MODE_MANUAL, + deps = [ + "packages/shovel/etc/config.json", + ], +) local_resource( "otterscan:base", @@ -475,14 +576,20 @@ local_resource( ), resource_deps = [ "yarn:install", - "anvil:mainnet", - "anvil:base", - "aa_bundler:base", "supabase", "supabase:generate", "wagmi:generate", "ui:build", - ], + "ui:generate-theme", + "daimo-expo-passkeys:build", + ] + ([ + "anvil:mainnet", + "anvil:base", + "aa_bundler:base", + "anvil:send-account-fixtures", + "anvil:anvil-add-send-merkle-drop-fixtures", + "anvil:anvil-add-token-paymaster-fixtures", + ] if not CI else []), serve_cmd = "" if CI else "yarn next-app dev", # In CI, playwright tests start the web server ) @@ -512,8 +619,10 @@ local_resource( local_resource( "caddy:web", + auto_init = not CI, labels = labels, serve_cmd = "caddy run --watch --config Caddyfile.dev", + trigger_mode = TRIGGER_MODE_MANUAL, deps = [ "Caddyfile.dev", ], @@ -529,8 +638,15 @@ local_resource( labels = labels, resource_deps = [ "yarn:install", - "aa_bundler:base", # TODO: remove once bundler tests are moved to playwright - "anvil:send-account-fixtures", # TODO: remove once bundler tests are moved to playwright + "contracts:build", + "wagmi:generate", + "supabase:generate", + "snaplet:generate", + "ui:build", + "ui:generate-theme", + "daimo-expo-passkeys:build", + "webauthn-authenticator:build", + "shovel:generate-config", ], deps = files_matching( @@ -577,6 +693,7 @@ local_resource( "anvil:send-account-fixtures", "aa_bundler:base", "snaplet:generate", + "next:web", "supabase", ], ) @@ -591,6 +708,7 @@ local_resource( "next:web", "playwright:deps", ], + trigger_mode = CI and TRIGGER_MODE_AUTO or TRIGGER_MODE_MANUAL, deps = files_matching( os.path.join("packages", "playwright"), lambda f: f.endswith(".ts"), @@ -602,7 +720,8 @@ cmd_button( argv = [ "yarn", "playwright", - "playwright show-report", + "playwright", + "show-report", ], icon_name = "info", location = location.RESOURCE, @@ -648,7 +767,10 @@ local_resource( "yarn supabase test", allow_parallel = True, labels = labels, - resource_deps = ["supabase"], + resource_deps = [ + "supabase", + "snaplet:generate", # hack to ensure snaplet doesn't include test pg_tap schema + ], deps = files_matching( os.path.join("supabase", "tests"), lambda f: f.endswith(".sql"), @@ -657,7 +779,7 @@ local_resource( local_resource( "contracts:test", - "yarn contracts test -vvv", + "yarn contracts test", allow_parallel = True, labels = labels, resource_deps = [ @@ -668,18 +790,31 @@ local_resource( ) local_resource( - "unit-tests:tests", - "echo 🥳", + "contracts:cov", + "yarn contracts test:cov -vvv", + allow_parallel = True, + labels = labels, + resource_deps = [ + "yarn:install", + "contracts:build", + "contracts:test", + ], + deps = contract_files, +) + +local_resource( + name = "unit-tests", allow_parallel = True, + cmd = "echo 🥳", labels = labels, resource_deps = [ # messy but create a single resource that runs all the tests "app:test", - "lint", "webauthn-authenticator:test", "supabase:test", "contracts:test", - "next:web", - ] + (["distributor:test"] if not CI else []), + "contracts:cov", + "distributor:test", + ], ) diff --git a/apps/distributor/docs/DEPLOY.md b/apps/distributor/docs/DEPLOY.md index f99314c68..fec1a0322 100644 --- a/apps/distributor/docs/DEPLOY.md +++ b/apps/distributor/docs/DEPLOY.md @@ -26,18 +26,28 @@ Then, ssh into the droplet and run the following commands. ## Install dependencies -### Install node 18 +### Install fnm and node ```shell sudo apt-get update -sudo apt-get install -y ca-certificates curl gnupg git -# Install node -sudo mkdir -p /etc/apt/keyrings -curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg -NODE_MAJOR=18 -echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list -sudo apt-get update -apt-get install nodejs=18.16.1-1nodesource1 -y +sudo apt-get install -y ca-certificates curl gnupg git unzip +curl -fsSL https://fnm.vercel.app/install | bash +source /root/.bashrc +fnm install +corepack enable +``` + +### Install Shovel + +We use shovel to aid in indexing onchain data. + +```shell +# linux/amd64, darwin/arm64, darwin/amd64, windows/amd64 +curl -LO https://indexsupply.net/bin/1.0/linux/amd64/shovel +chmod +x shovel +mv shovel /usr/local/bin/shovel +shovel -version +# v1.0 7602 ``` ### Install Caddy @@ -78,6 +88,7 @@ This uses development settings. For production, use the production settings. ```shell cat < .env.local # 🧑‍💻 DEVELOPMENT SETTING +NODE_ENV=development NEXT_PUBLIC_SUPABASE_PROJECT_ID=default NEXT_PUBLIC_SUPABASE_URL=http://localhost:54321 NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0 @@ -91,11 +102,13 @@ NEXT_PUBLIC_MAINNET_CHAIN_ID=1337 ### Running the app -Then, run the app: +#### Distributor + +This is the main application that recalculates the distribution shares for each Send token holder. Then, run the app: ```shell -pm2 start "yarn distributor start" -pm2 logs 0 +pm2 start --name distributor "yarn distributor start" +pm2 logs distributor ``` Check that the app is running: @@ -105,6 +118,33 @@ curl http://localhost:3050/distributor # {"distributor":true,"id":"keworl","lastBlockNumber":"18252625","lastBlockNumberAt":"2023-10-14T16:54:05.346Z","running":true} ``` +#### Shovel + +This is a background worker that listens for new Send token transfers and saves them to the database. It requires +some environment variables to be set. You can set them in a script and run the script. + +Ensure the `shovel` binary is installed and the `shovel` command is available in the terminal. Then, pass the path config file to the `shovel` command. + +```shell +cat < shovel.sh +#!/bin/bash +set -e + +export DASHBOARD_ROOT_PASSWORD=$(tr -dc 'A-Za-z0-9!?%=' < /dev/urandom | head -c 32) +export DATABASE_URL=postgres://postgres:postgres@localhost:54322/postgres +export BASE_NAME=basesepolia +export BASE_RPC_URL=https://base-sepolia-rpc.publicnode.com/ +export BASE_CHAIN_ID=84532 +export BASE_BLOCK_START=4570291 + +shovel -config /root/sendapp/packages/shovel/etc/config.json +EOF + +chmod +x shovel.sh +pm2 start --name shovel $(pwd)/shovel.sh +pm2 logs shovel +``` + #### ⚠️ If this is the first deployment, you should also run the following commands This will ensure that the app is restarted on reboot. @@ -132,7 +172,8 @@ caddy reload -c /etc/caddy/Caddyfile ```shell git pull yarn install -pm2 restart "yarn distributor start" +pm2 restart distributor +pm2 restart shovel ``` ## Monitoring diff --git a/apps/distributor/package.json b/apps/distributor/package.json index dc40540e2..cf8b6dfc3 100644 --- a/apps/distributor/package.json +++ b/apps/distributor/package.json @@ -9,7 +9,7 @@ "test": "test" }, "scripts": { - "test": "yarn _with-env yarn vitest", + "test": "yarn _with-env bun test", "_with-env": "dotenv -e ../../.env -c -- ", "start": " yarn _with-env bun --bun run ./src/server.ts", "dev": "yarn _with-env bun --bun run --watch ./src/server.ts" @@ -20,24 +20,20 @@ "dependencies": { "@my/supabase": "workspace:*", "@my/wagmi": "workspace:*", - "@supabase/supabase-js": "^2.38.5", - "@wagmi/core": "^2.6.3", + "@supabase/supabase-js": "^2.39.3", + "@wagmi/core": "2.6.5", "app": "workspace:*", "express": "^4.18.2", - "lru-cache": "^10.0.1", "pino": "^8.16.1", - "viem": "^2.7.6" + "viem": "^2.8.10" }, "devDependencies": { + "@types/bun": "latest", "@types/express": "^4", - "@types/node": "^20.11.0", "@types/supertest": "^2.0.16", - "bun-types": "latest", "debug": "^4.3.4", - "dotenv-cli": "^6.0.0", + "dotenv-cli": "^7.3.0", "supertest": "^6.3.3", - "ts-node": "^10.9.1", - "typescript": "^5.1.3", - "vitest": "^0.34.6" + "typescript": "^5.3.3" } } diff --git a/apps/distributor/src/app.ts b/apps/distributor/src/app.ts index e4798491d..9f791d622 100644 --- a/apps/distributor/src/app.ts +++ b/apps/distributor/src/app.ts @@ -1,5 +1,4 @@ -import * as path from 'path' -import express, { Request, Response, Router } from 'express' +import express, { type Request, type Response, Router } from 'express' import pino from 'pino' import { DistributorWorker } from './distributor' diff --git a/apps/distributor/src/distributor.test.ts b/apps/distributor/src/distributor.test.ts index 207f45db9..4ef6ec0c4 100644 --- a/apps/distributor/src/distributor.test.ts +++ b/apps/distributor/src/distributor.test.ts @@ -1,6 +1,10 @@ +// @ts-expect-error set __DEV__ for code shared between server and client +globalThis.__DEV__ = true + import request from 'supertest' -import { describe, expect, it } from 'vitest' +import { describe, expect, it } from 'bun:test' import app from './app' +import { supabaseAdmin } from './distributor' describe('Root Route', () => { it('should return correct response for the root route', async () => { @@ -30,11 +34,37 @@ describe('Distributor Route', () => { }) it('should perform distributor logic correctly', async () => { - const res = await request(app) - .post('/distributor') - .send({ id: 1 }) - .set('Authorization', `Bearer ${process.env.SUPABASE_SERVICE_ROLE}`) + const { data: distributions, error } = await supabaseAdmin + .from('distributions') + .select( + `*, + distribution_verification_values (*)` + ) + .lte('qualification_start', new Date().toISOString()) + .gte('qualification_end', new Date().toISOString()) - expect(res.statusCode).toBe(200) + if (error) { + throw error + } + + if (distributions.length === 0) { + throw new Error('No distributions found') + } + + expect(distributions.length).toBeGreaterThan(0) + + // get latest distribution id from API + let lastDistributionId: number + while (true) { + const res = await request(app).get('/distributor') + expect(res.statusCode).toBe(200) + if (res.body.lastDistributionId) { + lastDistributionId = res.body.lastDistributionId + break + } + await new Promise((resolve) => setTimeout(resolve, 1000)) + } + + expect(lastDistributionId).toBeDefined() }, 10_000) }) diff --git a/apps/distributor/src/distributor.ts b/apps/distributor/src/distributor.ts index 5067feb72..cd5b3fc04 100644 --- a/apps/distributor/src/distributor.ts +++ b/apps/distributor/src/distributor.ts @@ -1,11 +1,8 @@ -import { cpus } from 'os' -import { Database, Functions, Tables } from '@my/supabase/database.types' -import { sendTokenAbi, sendTokenAddress } from '@my/wagmi' +import { cpus } from 'node:os' +import type { Database, Functions, Tables } from '@my/supabase/database.types' +import { type sendTokenAddress, readSendTokenBalanceOf, config } from '@my/wagmi' import { createClient } from '@supabase/supabase-js' -import { LRUCache } from 'lru-cache' import type { Logger } from 'pino' -import { http, createPublicClient, getContract } from 'viem' -import { mainnet } from 'viem/chains' if (!process.env.NEXT_PUBLIC_SUPABASE_URL) { throw new Error( @@ -27,11 +24,6 @@ export const supabaseAdmin = createClient( { auth: { persistSession: false } } ) -const client = createPublicClient({ - chain: mainnet, - transport: http(process.env.NEXT_PUBLIC_MAINNET_RPC_URL), -}) - const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) const cpuCount = cpus().length @@ -51,123 +43,17 @@ function calculatePercentageWithBips(value: bigint, bips: bigint) { export class DistributorWorker { private log: Logger private running: boolean - private lastBlockNumber = 17579999n // send token deployment block - private lastBlockNumberAt: Date private id: string + private lastDistributionId: number | null = null private workerPromise: Promise - private blockTimestamps = new LRUCache<`0x${string}`, bigint>({ - max: 100, - }) constructor(log: Logger) { this.id = Math.random().toString(36).substring(7) this.log = log.child({ module: 'distributor', id: this.id }) this.running = true - this.lastBlockNumberAt = new Date() this.workerPromise = this.worker() } - private async saveTransfers(from: bigint, to: bigint) { - this.log.debug(`Getting transfers for block ${from}-${to}...`) - - const transfers = await client.getLogs({ - event: { - type: 'event', - inputs: [ - { - name: 'from', - type: 'address', - indexed: true, - }, - { - name: 'to', - type: 'address', - indexed: true, - }, - { - name: 'value', - type: 'uint256', - indexed: false, - }, - ], - name: 'Transfer', - }, - address: sendTokenAddress[client.chain.id], - strict: true, - fromBlock: from, - toBlock: to, - }) - - if (transfers.length === 0) { - this.log.debug('No transfers found.') - return - } - - this.log.debug(`Got ${transfers.length} transfers.`) - - // fetch the block timestamps in batches - const batches = inBatches(transfers).flatMap(async (transfers) => { - return await Promise.all( - transfers.map(async (transfer) => { - const { blockHash, blockNumber, transactionHash, logIndex, args } = transfer - const { from, to, value }: { from: string; to: string; value: bigint } = args - const blockTimestamp = await this.getBlockTimestamp(blockHash) - return { - block_hash: blockHash, - block_number: blockNumber.toString(), - block_timestamp: new Date(Number(blockTimestamp) * 1000), - tx_hash: transactionHash, - log_index: logIndex.toString(), - from, - to, - value: value.toString(), - } - }) - ) - }) - let rows: { - block_hash: `0x${string}` - block_number: string - block_timestamp: Date - tx_hash: `0x${string}` - log_index: string - from: string - to: string - value: string - }[] = [] - for await (const batch of batches) { - rows = rows.concat(...batch) - } - - if (rows.length === 0) { - this.log.debug('No transfers found.') - return [] - } - - this.log.debug({ rows: rows[0] }, `Saving ${rows.length} transfers...`) - - const { error } = await supabaseAdmin - .from('send_transfer_logs') - // @ts-expect-error supabase-js does not support bigint - .upsert(rows, { onConflict: 'block_hash,tx_hash,log_index' }) - - if (error) { - this.log.error({ error: error.message, code: error.code }, 'Error saving transfers.') - throw error - } - } - - private async getBlockTimestamp(blockHash: `0x${string}`) { - const cachedTimestamp = this.blockTimestamps.get(blockHash) - if (cachedTimestamp) { - return cachedTimestamp - } - const { timestamp } = await client.getBlock({ blockHash }) - this.blockTimestamps.set(blockHash, timestamp) - // biome-ignore lint/style/noNonNullAssertion: we know timestamp is defined - return this.blockTimestamps.get(blockHash)! - } - /** * Calculates distribution shares for distributions in qualification period. */ @@ -206,6 +92,17 @@ export class DistributorWorker { await this._calculateDistributionShares(distribution).catch((error) => errors.push(error)) } + if (distributions.length > 0) { + const lastDistribution = distributions[distributions.length - 1] + this.lastDistributionId = lastDistribution?.id ?? null + } else { + this.lastDistributionId = null + } + this.log.info( + { lastDistributionId: this.lastDistributionId }, + 'Finished calculating distributions.' + ) + if (errors.length > 0) { this.log.error(`Error calculating distribution shares. Encountered ${errors.length} errors.`) throw errors[0] @@ -220,6 +117,9 @@ export class DistributorWorker { distribution_verification_values: Tables<'distribution_verification_values'>[] } ) { + const log = this.log.child({ distribution_id: distribution.id }) + log.info({ distribution_id: distribution.id }, 'Calculating distribution shares.') + // fetch all verifications const verifications: Tables<'distribution_verifications'>[] = await (async () => { const _verifications: Tables<'distribution_verifications'>[] = [] @@ -235,10 +135,7 @@ export class DistributorWorker { .range(page, page + pageSize) if (error) { - this.log.error( - { error: error.message, code: error.code }, - 'Error fetching verifications.' - ) + log.error({ error: error.message, code: error.code }, 'Error fetching verifications.') throw error } @@ -253,8 +150,8 @@ export class DistributorWorker { return _verifications })() - this.log.info(`Found ${verifications.length} verifications.`) - this.log.debug({ verifications }) + log.info(`Found ${verifications.length} verifications.`) + log.debug({ verifications }) const verificationValues = distribution.distribution_verification_values.reduce( (acc, verification) => { @@ -278,8 +175,8 @@ export class DistributorWorker { {} as Record ) - this.log.info(`Found ${Object.keys(verificationsByUserId).length} users with verifications.`) - this.log.debug({ verificationsByUserId }) + log.info(`Found ${Object.keys(verificationsByUserId).length} users with verifications.`) + log.debug({ verificationsByUserId }) const hodlerAddresses: Functions<'distribution_hodler_addresses'> = await (async () => { const _hodlerAddresses: Functions<'distribution_hodler_addresses'> = [] @@ -300,7 +197,7 @@ export class DistributorWorker { .range(page, page + pageSize) if (error) { - this.log.error({ error: error.message, code: error.code }, 'Error fetching addresses.') + log.error({ error: error.message, code: error.code }, 'Error fetching addresses.') throw error } @@ -330,23 +227,23 @@ export class DistributorWorker { {} as Record ) - this.log.info(`Found ${hodlerAddresses.length} addresses.`) - this.log.debug({ hodlerAddresses }) + log.info(`Found ${hodlerAddresses.length} addresses.`) + log.debug({ hodlerAddresses }) // lookup balances of all hodler addresses in qualification period - const sendTokenContract = getContract({ - abi: sendTokenAbi, - address: sendTokenAddress[client.chain.id], - client, - }) - const batches = inBatches(hodlerAddresses).flatMap(async (addresses) => { return await Promise.all( addresses.map(async ({ user_id, address }) => { - const balance = await sendTokenContract.read.balanceOf([address as `0x${string}`]) + const balance = await readSendTokenBalanceOf(config, { + args: [address], + chainId: distribution.chain_id as keyof typeof sendTokenAddress, + blockNumber: distribution.snapshot_block_num + ? BigInt(distribution.snapshot_block_num) + : undefined, + }) return { user_id, - address: address as `0x${string}`, + address, balance: balance.toString(), } }) @@ -358,16 +255,18 @@ export class DistributorWorker { balances = balances.concat(...batch) } - this.log.info(`Found ${balances.length} balances.`) - this.log.debug({ balances }) + log.info(`Found ${balances.length} balances.`) + log.debug({ balances }) // Filter out hodler with not enough send token balance balances = balances.filter( ({ balance }) => BigInt(balance) >= BigInt(distribution.hodler_min_balance) ) - this.log.info(`Found ${balances.length} balances after filtering.`) - this.log.debug({ balances }) + log.info( + `Found ${balances.length} balances after filtering hodler_min_balance of ${distribution.hodler_min_balance}` + ) + log.debug({ balances }) // Calculate hodler pool share weights const amount = BigInt(distribution.amount) @@ -390,14 +289,14 @@ export class DistributorWorker { const hodlerPoolAvailableAmount = calculatePercentageWithBips(amount, hodlerPoolBips) const weightPerSend = (totalWeight * 10000n) / hodlerPoolAvailableAmount - this.log.info( + log.info( { totalWeight, hodlerPoolAvailableAmount, weightPerSend }, `Calculated ${Object.keys(poolWeights).length} weights.` ) - this.log.debug({ poolWeights }) + log.debug({ poolWeights }) if (totalWeight === 0n) { - this.log.warn('Total weight is 0. Skipping distribution.') + log.warn('Total weight is 0. Skipping distribution.') return } @@ -421,7 +320,7 @@ export class DistributorWorker { for (const [userId, verifications] of Object.entries(verificationsByUserId)) { const hodler = hodlerAddressesByUserId[userId] if (!hodler || !hodler.address) { - this.log.debug({ userId }, 'Hodler not found for user Skipping verification.') + log.debug({ userId }, 'Hodler not found for user Skipping verification.') continue } const { address } = hodler @@ -451,13 +350,13 @@ export class DistributorWorker { let totalBonusPoolAmount = 0n let totalFixedPoolAmount = 0n - this.log.info( + log.info( { maxBonusPoolBips, }, 'Calculated fixed & bonus pool amounts.' ) - this.log.debug({ hodlerShares, fixedPoolAmountsByAddress, bonusPoolBipsByAddress }) + log.debug({ hodlerShares, fixedPoolAmountsByAddress, bonusPoolBipsByAddress }) const shares = hodlerShares .map((share) => { @@ -472,11 +371,11 @@ export class DistributorWorker { totalFixedPoolAmount += fixedPoolAmount if (!userId) { - this.log.debug({ share }, 'Hodler not found for address. Skipping share.') + log.debug({ share }, 'Hodler not found for address. Skipping share.') return null } - this.log.debug( + log.debug( { address: share.address, balance: balancesByAddress[share.address], @@ -501,7 +400,7 @@ export class DistributorWorker { }) .filter(Boolean) - this.log.info( + log.info( { totalAmount, totalHodlerPoolAmount, @@ -513,14 +412,14 @@ export class DistributorWorker { }, 'Distribution totals' ) - this.log.info(`Calculated ${shares.length} shares.`) - this.log.debug({ shares }) + log.info(`Calculated ${shares.length} shares.`) + log.debug({ shares }) const { error } = await supabaseAdmin.rpc('update_distribution_shares', { distribution_id: distribution.id, shares, }) if (error) { - this.log.error({ error: error.message, code: error.code }, 'Error saving shares.') + log.error({ error: error.message, code: error.code }, 'Error saving shares.') throw error } return shares @@ -529,83 +428,16 @@ export class DistributorWorker { private async worker() { this.log.info('Starting distributor...', { id: this.id }) - // lookup last block number - const { data: latestTransfer, error } = await supabaseAdmin - .from('send_transfer_logs') - .select('*') - .order('block_number', { ascending: false }) - .limit(1) - .maybeSingle() - - if (error) { - this.log.error( - { error: error.message, code: error.code, details: error.details }, - 'Error fetching last block number.' - ) - if (error.code !== 'PGRST116') { - throw error - } - } - - if (latestTransfer) { - this.log.debug({ latestTransfer }, 'Found latest transfer.') - this.lastBlockNumber = BigInt(latestTransfer.block_number) - } else { - this.log.debug('No transfers found. Using default block number.') - } - - this.log.info(`Using last block number ${this.lastBlockNumber}.`) - - let latestBlockNumber = await client.getBlockNumber().catch((error) => { - this.log.error(error, 'Error fetching latest block number.') - process.exit(1) - }) - const cancel = client.watchBlocks({ - onBlock: async (block) => { - if (block.number <= this.lastBlockNumber) { - return - } - this.log.info(`New block ${block.number}`) - latestBlockNumber = block.number - }, - }) - while (this.running) { try { - if (latestBlockNumber <= this.lastBlockNumber) { - if (new Date().getTime() - this.lastBlockNumberAt.getTime() > 30000) { - this.log.warn('No new blocks found') - } - await sleep(client.pollingInterval) - continue - } - - this.log.info(`Processing block ${latestBlockNumber}`) - - // Always analyze back to finalized block handle reorgs/forked blocks - const { number: finalizedBlockNumber } = await client.getBlock({ blockTag: 'finalized' }) - const from = - this.lastBlockNumber < finalizedBlockNumber ? this.lastBlockNumber : finalizedBlockNumber - const to = latestBlockNumber - await this.saveTransfers(from, to) await this.calculateDistributions() - - // update last block number - this.lastBlockNumberAt = new Date() - this.lastBlockNumber = latestBlockNumber } catch (error) { this.log.error(error, `Error processing block. ${(error as Error).message}`) - await sleep(client.pollingInterval) - // skip to next block } + await sleep(60_000) // sleep for 1 minute } - cancel() - - this.log.info('Distributor stopped.', { - lastBlockNumber: this.lastBlockNumber, - lastBlockNumberAt: this.lastBlockNumberAt, - }) + this.log.info('Distributor stopped.') } public async stop() { @@ -614,18 +446,6 @@ export class DistributorWorker { return await this.workerPromise } - public isRunning() { - return this.running - } - - public getLastBlockNumber() { - return this.lastBlockNumber - } - - public getLastBlockNumberAt() { - return this.lastBlockNumberAt - } - public async calculateDistribution(id: string) { const { data: distribution, error } = await supabaseAdmin .from('distributions') @@ -650,10 +470,8 @@ export class DistributorWorker { public toJSON() { return { id: this.id, - lastBlockNumber: this.lastBlockNumber.toString(), - lastBlockNumberAt: this.lastBlockNumberAt.toISOString(), running: this.running, - calculateDistribution: this.calculateDistribution.bind(this), + lastDistributionId: this.lastDistributionId, } } } diff --git a/apps/distributor/src/server.ts b/apps/distributor/src/server.ts index f51d0ce4d..2374742a6 100644 --- a/apps/distributor/src/server.ts +++ b/apps/distributor/src/server.ts @@ -1,7 +1,10 @@ +// @ts-expect-error set __DEV__ for code shared between server and client +globalThis.__DEV__ = process.env.NODE_ENV !== 'production' + import app from './app' const PORT = process.env.PORT || 3050 app.listen(PORT, () => { - console.log(`Server running on port ${PORT}`) + console.log(`Server running. PORT=${PORT} __DEV__=${__DEV__}`) }) diff --git a/apps/distributor/tsconfig.json b/apps/distributor/tsconfig.json index a3bd4c573..781bccc9e 100644 --- a/apps/distributor/tsconfig.json +++ b/apps/distributor/tsconfig.json @@ -1,7 +1,4 @@ { - "extends": "../../tsconfig.json", - "compilerOptions": { - "types": ["bun-types"] - }, - "include": ["./src/**/*"] + "extends": "../../tsconfig.base.json", + "include": ["./src", "../../globals.d.ts"] } diff --git a/apps/expo/metro.config.js b/apps/expo/metro.config.js index 3c064a4b5..7c0422f99 100644 --- a/apps/expo/metro.config.js +++ b/apps/expo/metro.config.js @@ -3,7 +3,7 @@ * @type {import('expo/metro-config')} */ const { getDefaultConfig } = require('@expo/metro-config') -const path = require('path') +const path = require('node:path') const projectRoot = __dirname const workspaceRoot = path.resolve(__dirname, '../..') diff --git a/apps/expo/package.json b/apps/expo/package.json index 8b77a1a28..c49dd1fb7 100644 --- a/apps/expo/package.json +++ b/apps/expo/package.json @@ -11,48 +11,56 @@ }, "dependencies": { "@babel/runtime": "^7.18.9", - "@expo/config-plugins": "~7.2.2", + "@expo/config-plugins": "~7.8.0", "@my/ui": "workspace:*", + "@react-native-async-storage/async-storage": "1.21.0", "@react-native-community/netinfo": "^11.2.1", "@react-navigation/native": "^6.1.6", "app": "workspace:*", "babel-plugin-module-resolver": "^4.1.0", - "burnt": "^0.11.7", - "expo": "^49.0.0", - "expo-constants": "~14.4.2", - "expo-dev-client": "~2.4.11", - "expo-font": "~11.4.0", - "expo-image": "~1.3.3", - "expo-image-picker": "~14.3.1", - "expo-linear-gradient": "~12.3.0", - "expo-linking": "~5.0.2", - "expo-router": "^2.0.0", - "expo-splash-screen": "~0.20.5", - "expo-status-bar": "~1.6.0", - "expo-updates": "~0.18.16", + "burnt": "^0.12.1", + "expo": "^50.0.0", + "expo-build-properties": "~0.11.1", + "expo-clipboard": "^5.0.1", + "expo-constants": "~15.4.5", + "expo-crypto": "~12.8.0", + "expo-dev-client": "~3.3.8", + "expo-device": "~5.9.3", + "expo-font": "~11.10.2", + "expo-image": "~1.10.5", + "expo-image-picker": "~14.7.1", + "expo-linear-gradient": "~12.7.1", + "expo-linking": "~6.2.2", + "expo-notifications": "~0.27.6", + "expo-router": "~3.4.6", + "expo-secure-store": "~12.8.1", + "expo-splash-screen": "~0.26.4", + "expo-status-bar": "~1.11.1", + "expo-system-ui": "~2.9.3", + "expo-updates": "~0.24.9", + "expo-web-browser": "~12.8.2", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-native": "0.72.5", - "react-native-gesture-handler": "~2.12.0", - "react-native-safe-area-context": "4.6.3", - "react-native-screens": "~3.22.0", - "react-native-svg": "13.9.0", - "react-native-web": "~0.19.6" + "react-native": "0.73.6", + "react-native-dotenv": "^3.4.9", + "react-native-gesture-handler": "~2.14.1", + "react-native-ios-modal": "^0.1.8", + "react-native-reanimated": "~3.6.2", + "react-native-safe-area-context": "4.8.2", + "react-native-screens": "~3.29.0", + "react-native-svg": "14.1.0", + "react-native-url-polyfill": "^1.3.0", + "react-native-web": "~0.19.10" }, "devDependencies": { - "@babel/core": "^7.17.9", - "@expo/metro-config": "~0.10.0", - "@tamagui/babel-plugin": "^1.88.8", + "@babel/core": "^7.20.2", + "@expo/metro-config": "~0.17.1", + "@tamagui/babel-plugin": "^1.91.4", "babel-plugin-transform-inline-environment-variables": "^0.4.4", + "babel-preset-expo": "^10.0.1", + "cross-env": "^7.0.3", "metro-minify-terser": "^0.74.1", - "typescript": "^5.1.3" - }, - "resolutions": { - "metro": "0.76.0", - "metro-resolver": "0.76.0" - }, - "overrides": { - "metro": "0.76.0", - "metro-resolver": "0.76.0" + "react-native-clean-project": "^4.0.3", + "typescript": "^5.3.3" } } diff --git a/apps/next/next.config.js b/apps/next/next.config.js index b1f2d5320..0e2534959 100644 --- a/apps/next/next.config.js +++ b/apps/next/next.config.js @@ -1,6 +1,6 @@ /** @type {import('next').NextConfig} */ import tamaguiPlugin from '@tamagui/next-plugin' -import { join } from 'path' +import { join } from 'node:path' import withPlaiceholder from '@plaiceholder/next' const boolVals = { @@ -80,6 +80,8 @@ export default () => { 'react-native-web', 'expo-linking', 'expo-constants', + 'expo-clipboard', + 'expo-sharing', 'expo-modules-core', 'expo-device', 'expo-image-picker', diff --git a/apps/next/package.json b/apps/next/package.json index b9b3b2a31..b73da9b01 100644 --- a/apps/next/package.json +++ b/apps/next/package.json @@ -15,10 +15,11 @@ }, "dependencies": { "@plaiceholder/next": "^3.0.0", + "@rainbow-me/rainbowkit": "^2.0.4", "@supabase/auth-helpers-react": "^0.4.2", "@supabase/ssr": "^0.0.9", - "@supabase/supabase-js": "^2.38.5", - "@tamagui/next-theme": "^1.88.8", + "@supabase/supabase-js": "^2.39.3", + "@tamagui/next-theme": "^1.93.2", "@tanstack/react-query": "^5.18.1", "@tanstack/react-query-devtools": "^5.18.1", "@trpc/client": "11.0.0-next-beta.264", @@ -26,14 +27,15 @@ "@trpc/react-query": "11.0.0-next-beta.264", "@trpc/server": "11.0.0-next-beta.264", "app": "workspace:*", + "expo-sharing": "~11.5.0", "next": "13.4.19", "plaiceholder": "^3.0.0", "raf": "^3.4.1", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-native": "0.72.5", - "react-native-reanimated": "~3.3.0", - "react-native-web": "~0.19.6", + "react-native": "0.73.6", + "react-native-reanimated": "~3.6.2", + "react-native-web": "~0.19.10", "react-native-web-lite": "^1.74.8", "sharp": "0.32.6", "vercel": "^33.5.2", @@ -42,10 +44,10 @@ "devDependencies": { "@next/bundle-analyzer": "^13.4.19", "@next/eslint-plugin-next": "^14.0.4", - "@tamagui/next-plugin": "^1.88.8", + "@tamagui/next-plugin": "^1.93.2", "@types/node": "^20.11.0", "@welldone-software/why-did-you-render": "^7.0.1", - "dotenv-cli": "^6.0.0", + "dotenv-cli": "^7.3.0", "eslint-config-next": "^14.0.4" } } diff --git a/apps/next/pages/_app.tsx b/apps/next/pages/_app.tsx index 8384e174c..abbacaed3 100644 --- a/apps/next/pages/_app.tsx +++ b/apps/next/pages/_app.tsx @@ -1,19 +1,19 @@ +import '@rainbow-me/rainbowkit/styles.css' import '../public/reset.css' +import '../styles/globals.css' -import '@tamagui/font-inter/css/400.css' -import '@tamagui/font-inter/css/700.css' import 'raf/polyfill' +import '@my/ui/src/config/fonts.css' -import { ColorScheme, NextThemeProvider, useRootTheme } from '@tamagui/next-theme' -import { Provider } from 'app/provider' -import { AuthProviderProps } from 'app/provider/auth' +import { type ColorScheme, NextThemeProvider, useRootTheme } from '@tamagui/next-theme' + +import type { AuthProviderProps } from 'app/provider/auth' import { api } from 'app/utils/api' -import { NextPage } from 'next' +import type { NextPage } from 'next' import Head from 'next/head' - -import { ReactElement, ReactNode } from 'react' +import type { ReactElement, ReactNode } from 'react' import type { SolitoAppProps } from 'solito' -import Favicons from './favicons' +import { Provider } from 'app/provider' if (process.env.NODE_ENV === 'production') { require('../public/tamagui.css') @@ -29,8 +29,8 @@ function MyApp({ }: SolitoAppProps<{ initialSession: AuthProviderProps['initialSession'] }>) { // reference: https://nextjs.org/docs/pages/building-your-application/routing/pages-and-layouts const getLayout = Component.getLayout || ((page) => page) - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [theme, setTheme] = useRootTheme() + + const [, setTheme] = useRootTheme() return ( <> @@ -44,8 +44,15 @@ function MyApp({ name="viewport" content="width=320, initial-scale=1, minimum-scale=1, maximum-scale=1" /> - - + + + + + + + + + { diff --git a/apps/next/pages/_document.tsx b/apps/next/pages/_document.tsx index 4a3d57d15..1bcd883aa 100644 --- a/apps/next/pages/_document.tsx +++ b/apps/next/pages/_document.tsx @@ -1,6 +1,6 @@ import NextDocument, { - DocumentContext, - DocumentInitialProps, + type DocumentContext, + type DocumentInitialProps, Head, Html, Main, diff --git a/apps/next/pages/settings/index.tsx b/apps/next/pages/account/index.tsx similarity index 50% rename from apps/next/pages/settings/index.tsx rename to apps/next/pages/account/index.tsx index 648cf4833..cfc4c8a12 100644 --- a/apps/next/pages/settings/index.tsx +++ b/apps/next/pages/account/index.tsx @@ -1,30 +1,30 @@ import { HomeLayout } from 'app/features/home/layout.web' -import { SettingsLayout } from 'app/features/settings/layout.web' -import { GeneralSettingsScreen } from 'app/features/settings/general-screen' import Head from 'next/head' import { userProtectedGetSSP } from 'utils/userProtected' -import { NextPageWithLayout } from '../_app' +import type { NextPageWithLayout } from '../_app' +import { AccountScreen } from 'app/features/account/screen' +import { ButtonOption, TopNav } from 'app/components/TopNav' export const Page: NextPageWithLayout = () => { return ( <> - Settings + Send | Account - + ) } export const getServerSideProps = userProtectedGetSSP() Page.getLayout = (children) => ( - - {children} + }> + {children} ) diff --git a/apps/next/pages/account/rewards.tsx b/apps/next/pages/account/rewards.tsx new file mode 100644 index 000000000..adcc2caca --- /dev/null +++ b/apps/next/pages/account/rewards.tsx @@ -0,0 +1,34 @@ +import { RewardsScreen } from 'app/features/account/rewards/screen' +import Head from 'next/head' +import { userProtectedGetSSP } from 'utils/userProtected' +import type { NextPageWithLayout } from 'next-app/pages/_app' +import { HomeLayout } from 'app/features/home/layout.web' +import { ButtonOption, TopNav } from 'app/components/TopNav' + +export const Page: NextPageWithLayout = () => { + return ( + <> + + Send | Send it Rewards + + + + ) +} + +export const getServerSideProps = userProtectedGetSSP() + +const subheader = + 'Register at least 1 Sendtag, maintain the minimum balance, avoid selling, and refer others for a bonus multiplier. ' + +Page.getLayout = (children) => ( + + } + > + {children} + +) + +export default Page diff --git a/apps/next/pages/account/sendtag/checkout.tsx b/apps/next/pages/account/sendtag/checkout.tsx new file mode 100644 index 000000000..2b60448cd --- /dev/null +++ b/apps/next/pages/account/sendtag/checkout.tsx @@ -0,0 +1,36 @@ +import { CheckoutScreen } from 'app/features/account/sendtag/checkout/screen' +import Head from 'next/head' +import { userProtectedGetSSP } from 'utils/userProtected' +import type { NextPageWithLayout } from '../../_app' +import { HomeLayout } from 'app/features/home/layout.web' +import { ButtonOption, TopNav } from 'app/components/TopNav' + +export const Page: NextPageWithLayout = () => { + return ( + <> + + Send | Sendtag Checkout + + + + + ) +} + +export const getServerSideProps = userProtectedGetSSP() + +const subheader = 'Sendtags are usernames within the Send platform. You may register up to 5.' + +Page.getLayout = (children) => ( + } + > + {children} + +) + +export default Page diff --git a/apps/next/pages/account/sendtag/index.tsx b/apps/next/pages/account/sendtag/index.tsx new file mode 100644 index 000000000..6eb8fda3c --- /dev/null +++ b/apps/next/pages/account/sendtag/index.tsx @@ -0,0 +1,32 @@ +import Head from 'next/head' +import { userProtectedGetSSP } from 'utils/userProtected' +import type { NextPageWithLayout } from '../../_app' +import { HomeLayout } from 'app/features/home/layout.web' +import { SendTagScreen } from 'app/features/account/sendtag/screen' +import { ButtonOption, TopNav } from 'app/components/TopNav' + +export const Page: NextPageWithLayout = () => { + return ( + <> + + Send | Sendtag + + + + + ) +} + +export const getServerSideProps = userProtectedGetSSP() + +const subheader = 'Sendtags are usernames within the Send platform. You may register up to 5.' + +Page.getLayout = (children) => ( + } + > + {children} + +) + +export default Page diff --git a/apps/next/pages/account/settings/edit-profile.tsx b/apps/next/pages/account/settings/edit-profile.tsx new file mode 100644 index 000000000..e6ae230b5 --- /dev/null +++ b/apps/next/pages/account/settings/edit-profile.tsx @@ -0,0 +1,28 @@ +import { HomeLayout } from 'app/features/home/layout.web' +import { SettingsLayout } from 'app/features/account/settings/layout.web' +import { EditProfileScreen } from 'app/features/account/settings/edit-profile' +import Head from 'next/head' +import { userProtectedGetSSP } from 'utils/userProtected' +import type { NextPageWithLayout } from '../../_app' +import { ButtonOption, TopNav } from 'app/components/TopNav' + +export const Page: NextPageWithLayout = () => { + return ( + <> + + Send | Edit Profile + + + + + ) +} + +export const getServerSideProps = userProtectedGetSSP() +Page.getLayout = (children) => ( + }> + {children} + +) + +export default Page diff --git a/apps/next/pages/account/settings/personal-info.tsx b/apps/next/pages/account/settings/personal-info.tsx new file mode 100644 index 000000000..db832c64b --- /dev/null +++ b/apps/next/pages/account/settings/personal-info.tsx @@ -0,0 +1,28 @@ +import { HomeLayout } from 'app/features/home/layout.web' +import { SettingsLayout } from 'app/features/account/settings/layout.web' +import { PersonalInfoScreen } from 'app/features/account/settings' +import Head from 'next/head' +import { userProtectedGetSSP } from 'utils/userProtected' +import type { NextPageWithLayout } from '../../_app' +import { ButtonOption, TopNav } from 'app/components/TopNav' + +export const Page: NextPageWithLayout = () => { + return ( + <> + + Send | Personal Information + + + + + ) +} + +export const getServerSideProps = userProtectedGetSSP() +Page.getLayout = (children) => ( + }> + {children} + +) + +export default Page diff --git a/apps/next/pages/account/settings/support.tsx b/apps/next/pages/account/settings/support.tsx new file mode 100644 index 000000000..5c5a7dee8 --- /dev/null +++ b/apps/next/pages/account/settings/support.tsx @@ -0,0 +1,29 @@ +import { HomeLayout } from 'app/features/home/layout.web' +import { SettingsLayout } from 'app/features/account/settings/layout.web' +import { SupportScreen } from 'app/features/account/settings' +import Head from 'next/head' +import { userProtectedGetSSP } from 'utils/userProtected' +import type { NextPageWithLayout } from '../../_app' +import { ButtonOption, TopNav } from 'app/components/TopNav' + +export const Page: NextPageWithLayout = () => { + return ( + <> + + Send | Support + + + + + ) +} + +export const getServerSideProps = userProtectedGetSSP() + +Page.getLayout = (children) => ( + }> + {children} + +) + +export default Page diff --git a/apps/next/pages/activity.tsx b/apps/next/pages/activity.tsx index 2aafa84d6..cc4b637f7 100644 --- a/apps/next/pages/activity.tsx +++ b/apps/next/pages/activity.tsx @@ -2,7 +2,8 @@ import { ActivityScreen } from 'app/features/activity/screen' import { HomeLayout } from 'app/features/home/layout.web' import Head from 'next/head' import { userProtectedGetSSP } from 'utils/userProtected' -import { NextPageWithLayout } from './_app' +import type { NextPageWithLayout } from './_app' +import { ButtonOption, TopNav } from 'app/components/TopNav' export const Page: NextPageWithLayout = () => { return ( @@ -15,8 +16,13 @@ export const Page: NextPageWithLayout = () => { ) } -export const getServerSideProps = userProtectedGetSSP() +export const getServerSideProps = userProtectedGetSSP(async () => { + // disable for now + return { redirect: { destination: '/', permanent: false } } +}) -Page.getLayout = (children) => {children} +Page.getLayout = (children) => ( + }>{children} +) export default Page diff --git a/apps/next/pages/api/auth/callback.ts b/apps/next/pages/api/auth/callback.ts index 4fd710c36..b4e7a1ae3 100644 --- a/apps/next/pages/api/auth/callback.ts +++ b/apps/next/pages/api/auth/callback.ts @@ -1,5 +1,5 @@ import { createPagesServerClient } from '@supabase/auth-helpers-nextjs' -import { NextApiHandler } from 'next' +import type { NextApiHandler } from 'next' const handler: NextApiHandler = async (req, res) => { const { code } = req.query diff --git a/apps/next/pages/api/healthz.ts b/apps/next/pages/api/healthz.ts index 759fa16c3..6f21f5bd1 100644 --- a/apps/next/pages/api/healthz.ts +++ b/apps/next/pages/api/healthz.ts @@ -1,4 +1,4 @@ -import { NextApiRequest, NextApiResponse } from 'next' +import type { NextApiRequest, NextApiResponse } from 'next' export default function handler(_: NextApiRequest, res: NextApiResponse) { res.status(200).json('ok') diff --git a/apps/next/pages/auth/onboarding.tsx b/apps/next/pages/auth/onboarding.tsx new file mode 100644 index 000000000..4aad8f36c --- /dev/null +++ b/apps/next/pages/auth/onboarding.tsx @@ -0,0 +1,48 @@ +import { OnboardingScreen } from 'app/features/auth/onboarding/screen' +import Head from 'next/head' +import { userProtectedGetSSP } from 'utils/userProtected' +import type { NextPageWithLayout } from '../_app' +import { AuthLayout } from 'app/features/auth/layout.web' +import { useContext, useEffect } from 'react' +import { AuthCarouselContext } from 'app/features/auth/AuthCarouselContext' +// import { getRemoteAssets } from 'utils/getRemoteAssets' +import type { InferGetServerSidePropsType } from 'next' + +export const Page: NextPageWithLayout> = ({ + images, +}) => { + const { carouselImages, setCarouselImages } = useContext(AuthCarouselContext) + + useEffect(() => { + if (carouselImages.length === 0) setCarouselImages(images) + }, [setCarouselImages, carouselImages, images]) + + return ( + <> + + Send | Onboarding + + + + ) +} + +export const getServerSideProps = userProtectedGetSSP( + async () => { + // disable for now + return { redirect: { destination: '/', permanent: false } } + } + /*async () => { + const paths = [ + 'app_images/auth_image_1.jpg?raw=true', + 'app_images/auth_image_2.jpg?raw=true', + 'app_images/auth_image_3.jpg?raw=true', + ] + const images = await getRemoteAssets(paths) + return { props: { images } } +}*/ +) + +Page.getLayout = (children) => {children} + +export default Page diff --git a/apps/next/pages/sign-in.tsx b/apps/next/pages/auth/sign-in.tsx similarity index 50% rename from apps/next/pages/sign-in.tsx rename to apps/next/pages/auth/sign-in.tsx index 051de5300..77dbbc86a 100644 --- a/apps/next/pages/sign-in.tsx +++ b/apps/next/pages/auth/sign-in.tsx @@ -1,10 +1,10 @@ -import { SignInScreen } from 'app/features/auth/sign-in-screen' +import { SignInScreen } from 'app/features/auth/sign-in/screen' import Head from 'next/head' import { guestOnlyGetSSP } from 'utils/guestOnly' -import { NextPageWithLayout } from './_app' +import type { NextPageWithLayout } from '../_app' import { AuthLayout } from 'app/features/auth/layout.web' -import { getPlaiceholderImage } from 'app/utils/getPlaiceholderImage' -import { InferGetServerSidePropsType } from 'next' +import type { InferGetServerSidePropsType } from 'next' +import { getRemoteAssets } from 'utils/getRemoteAssets' import { useContext, useEffect } from 'react' import { AuthCarouselContext } from 'app/features/auth/AuthCarouselContext' @@ -21,11 +21,7 @@ export const Page: NextPageWithLayout Send | Sign In - + @@ -33,18 +29,13 @@ export const Page: NextPageWithLayout { - const remoteImagePath = 'https://github.com/0xsend/assets/blob/main/app_images' - const remoteImageQueryParams = '?raw=true' - const images = [ - await getPlaiceholderImage(`${remoteImagePath}/auth_image_1.jpg${remoteImageQueryParams}`), - await getPlaiceholderImage(`${remoteImagePath}/auth_image_2.jpg${remoteImageQueryParams}`), - await getPlaiceholderImage(`${remoteImagePath}/auth_image_3.jpg${remoteImageQueryParams}`), + const paths = [ + 'app_images/auth_image_1.jpg?raw=true', + 'app_images/auth_image_2.jpg?raw=true', + 'app_images/auth_image_3.jpg?raw=true', ] - return { - props: { - images, - }, - } + const images = await getRemoteAssets(paths) + return { props: { images } } }) Page.getLayout = (children) => {children} diff --git a/apps/next/pages/checkout.tsx b/apps/next/pages/checkout.tsx deleted file mode 100644 index 829b7819f..000000000 --- a/apps/next/pages/checkout.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { CheckoutScreen } from 'app/features/checkout/screen' -import Head from 'next/head' -import { userProtectedGetSSP } from 'utils/userProtected' -import { NextPageWithLayout } from './_app' - -export const Page: NextPageWithLayout = () => { - return ( - <> - - Send Tag Checkout - - - - - ) -} - -export const getServerSideProps = userProtectedGetSSP() - -export default Page diff --git a/apps/next/pages/distributions.tsx b/apps/next/pages/distributions.tsx deleted file mode 100644 index f854607f5..000000000 --- a/apps/next/pages/distributions.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { DistributionsScreen } from 'app/features/distributions/screen' -import Head from 'next/head' -import { userProtectedGetSSP } from 'utils/userProtected' -import { NextPageWithLayout } from './_app' -import { HomeLayout } from 'app/features/home/layout.web' - -export const Page: NextPageWithLayout = () => { - return ( - <> - - Send | Distributions - - - - ) -} - -export const getServerSideProps = userProtectedGetSSP() - -Page.getLayout = (children) => {children} - -export default Page diff --git a/apps/next/pages/favicons.tsx b/apps/next/pages/favicons.tsx deleted file mode 100644 index 75111f45a..000000000 --- a/apps/next/pages/favicons.tsx +++ /dev/null @@ -1,69 +0,0 @@ -const Favicons = () => ( - <> - - - - - - - - - - - - - -) -export default Favicons diff --git a/apps/next/pages/index.tsx b/apps/next/pages/index.tsx index 30c1b6c5b..786e48e92 100644 --- a/apps/next/pages/index.tsx +++ b/apps/next/pages/index.tsx @@ -1,19 +1,18 @@ -import { HomeSideBarWrapper } from 'app/components/sidebar/HomeSideBar' import { HomeScreen } from 'app/features/home/screen' -import { GetServerSidePropsContext } from 'next' +import { HomeLayout } from 'app/features/home/layout.web' +import type { GetServerSidePropsContext } from 'next' import Head from 'next/head' import { userProtectedGetSSP } from 'utils/userProtected' -import { NextPageWithLayout } from './_app' +import type { NextPageWithLayout } from './_app' +import { ButtonOption, TopNav } from 'app/components/TopNav' export const Page: NextPageWithLayout = () => { return ( <> - Home + Send | Home - - - + ) } @@ -35,4 +34,10 @@ function setReferralCodeCookie(context: GetServerSidePropsContext) { } } +Page.getLayout = (children) => ( + }> + {children} + +) + export default Page diff --git a/apps/next/pages/leaderboard.tsx b/apps/next/pages/leaderboard.tsx index 73f774161..3bdfc8313 100644 --- a/apps/next/pages/leaderboard.tsx +++ b/apps/next/pages/leaderboard.tsx @@ -2,7 +2,8 @@ import { LeaderboardScreen } from 'app/features/leaderboard/screen' import { HomeLayout } from 'app/features/home/layout.web' import Head from 'next/head' import { userProtectedGetSSP } from 'utils/userProtected' -import { NextPageWithLayout } from './_app' +import type { NextPageWithLayout } from './_app' +import { TopNav } from 'app/components/TopNav' export const Page: NextPageWithLayout = () => { return ( @@ -15,8 +16,13 @@ export const Page: NextPageWithLayout = () => { ) } -export const getServerSideProps = userProtectedGetSSP() +export const getServerSideProps = userProtectedGetSSP(async () => { + // disable for now + return { redirect: { destination: '/', permanent: false } } +}) -Page.getLayout = (children) => {children} +Page.getLayout = (children) => ( + }>{children} +) export default Page diff --git a/apps/next/pages/onboarding.tsx b/apps/next/pages/onboarding.tsx deleted file mode 100644 index accd7dc40..000000000 --- a/apps/next/pages/onboarding.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { OnboardingScreen } from 'app/features/onboarding/screen' -import Head from 'next/head' -import { userProtectedGetSSP } from 'utils/userProtected' -import { NextPageWithLayout } from './_app' - -export const Page: NextPageWithLayout = () => { - return ( - <> - - Send | Onboarding - - - - ) -} - -export const getServerSideProps = userProtectedGetSSP() -export default Page diff --git a/apps/next/pages/profile/[tag].tsx b/apps/next/pages/profile/[tag].tsx index 7170472f8..29384083a 100644 --- a/apps/next/pages/profile/[tag].tsx +++ b/apps/next/pages/profile/[tag].tsx @@ -2,13 +2,14 @@ import { ProfileScreen } from 'app/features/profile/screen' import { HomeLayout } from 'app/features/home/layout.web' import Head from 'next/head' import { createPagesServerClient } from '@supabase/auth-helpers-nextjs' -import { NextPageWithLayout } from '../_app' -import { GetServerSideProps, GetServerSidePropsContext } from 'next' -import { Database } from '@my/supabase/database.types' +import type { NextPageWithLayout } from '../_app' +import type { GetServerSideProps, GetServerSidePropsContext } from 'next' +import type { Database } from '@my/supabase/database.types' import { userOnboarded } from 'utils/userOnboarded' -import { CheckoutTagSchema } from 'app/features/checkout/CheckoutTagSchema' +import { CheckoutTagSchema } from 'app/features/account/sendtag/checkout/CheckoutTagSchema' import { assert } from 'app/utils/assert' import { supabaseAdmin } from 'app/utils/supabase/admin' +import { TopNav } from 'app/components/TopNav' export const Page: NextPageWithLayout = () => { return ( @@ -23,6 +24,9 @@ export const Page: NextPageWithLayout = () => { // Profile page is not protected, but we need to look up the user profile by tag in case we have to show a 404 export const getServerSideProps = (async (ctx: GetServerSidePropsContext) => { + // disable for now + return { redirect: { destination: '/', permanent: false } } + /* const { tag } = ctx.params ?? {} // ensure tag is valid before proceeding @@ -73,8 +77,11 @@ export const getServerSideProps = (async (ctx: GetServerSidePropsContext) => { return { props: {}, } + */ }) satisfies GetServerSideProps -Page.getLayout = (children) => {children} +Page.getLayout = (children) => ( + }>{children} +) export default Page diff --git a/apps/next/pages/qr-scan.tsx b/apps/next/pages/qr-scan.tsx index f629c779a..8b4d01fd8 100644 --- a/apps/next/pages/qr-scan.tsx +++ b/apps/next/pages/qr-scan.tsx @@ -1,13 +1,13 @@ import { QRScreen } from 'app/features/send/screens/qrscan' import Head from 'next/head' import { guestOnlyGetSSP } from 'utils/guestOnly' -import { NextPageWithLayout } from './_app' +import type { NextPageWithLayout } from './_app' export const Page: NextPageWithLayout = () => { return ( <> - QRScan + Send | QRScan @@ -15,6 +15,9 @@ export const Page: NextPageWithLayout = () => { ) } -export const getServerSideProps = guestOnlyGetSSP() +export const getServerSideProps = guestOnlyGetSSP(async () => { + // disable for now + return { redirect: { destination: '/', permanent: false } } +}) export default Page diff --git a/apps/next/pages/receive.tsx b/apps/next/pages/receive.tsx index c63ff2129..d7d90b0ab 100644 --- a/apps/next/pages/receive.tsx +++ b/apps/next/pages/receive.tsx @@ -1,13 +1,13 @@ import { ReceiveScreen } from 'app/features/send/screens/receive' import Head from 'next/head' import { userProtectedGetSSP } from 'utils/userProtected' -import { NextPageWithLayout } from './_app' +import type { NextPageWithLayout } from './_app' export const Page: NextPageWithLayout = () => { return ( <> - Receive + Send | Receive @@ -15,6 +15,9 @@ export const Page: NextPageWithLayout = () => { ) } -export const getServerSideProps = userProtectedGetSSP() +export const getServerSideProps = userProtectedGetSSP(async () => { + // disable for now + return { redirect: { destination: '/', permanent: false } } +}) export default Page diff --git a/apps/next/pages/referrals.tsx b/apps/next/pages/referrals.tsx index bb877695d..33f5e8883 100644 --- a/apps/next/pages/referrals.tsx +++ b/apps/next/pages/referrals.tsx @@ -1,8 +1,9 @@ import Head from 'next/head' import { userProtectedGetSSP } from 'utils/userProtected' -import { NextPageWithLayout } from './_app' +import type { NextPageWithLayout } from './_app' import { ReferralsScreen } from 'app/features/referrals/screen' import { HomeLayout } from 'app/features/home/layout.web' +import { TopNav } from 'app/components/TopNav' export const Page: NextPageWithLayout = () => { return ( @@ -16,8 +17,13 @@ export const Page: NextPageWithLayout = () => { ) } -export const getServerSideProps = userProtectedGetSSP() +export const getServerSideProps = userProtectedGetSSP(async () => { + // disable for now + return { redirect: { destination: '/', permanent: false } } +}) -Page.getLayout = (children) => {children} +Page.getLayout = (children) => ( + }>{children} +) export default Page diff --git a/apps/next/pages/secret-shop.tsx b/apps/next/pages/secret-shop.tsx new file mode 100644 index 000000000..a772edbc2 --- /dev/null +++ b/apps/next/pages/secret-shop.tsx @@ -0,0 +1,24 @@ +import { SecretShopScreen } from 'app/features/secret-shop/screen' +import { HomeLayout } from 'app/features/home/layout.web' +import Head from 'next/head' +import { userProtectedGetSSP } from 'utils/userProtected' +import type { NextPageWithLayout } from './_app' +import { TopNav } from 'app/components/TopNav' + +export const Page: NextPageWithLayout = () => { + return ( + <> + + Send | Secret Shop + + + + ) +} +export const getServerSideProps = userProtectedGetSSP() + +Page.getLayout = (children) => ( + }>{children} +) + +export default Page diff --git a/apps/next/pages/send.tsx b/apps/next/pages/send.tsx index 9dd16261e..54e7ca14a 100644 --- a/apps/next/pages/send.tsx +++ b/apps/next/pages/send.tsx @@ -1,7 +1,7 @@ import { SendScreen } from 'app/features/send/screens/send' import Head from 'next/head' import { userProtectedGetSSP } from 'utils/userProtected' -import { NextPageWithLayout } from './_app' +import type { NextPageWithLayout } from './_app' export const Page: NextPageWithLayout = () => { return ( @@ -15,6 +15,9 @@ export const Page: NextPageWithLayout = () => { ) } -export const getServerSideProps = userProtectedGetSSP() +export const getServerSideProps = userProtectedGetSSP(async () => { + // disable for now + return { redirect: { destination: '/', permanent: false } } +}) export default Page diff --git a/apps/next/pages/settings/change-phone.tsx b/apps/next/pages/settings/change-phone.tsx deleted file mode 100644 index 2eae53a57..000000000 --- a/apps/next/pages/settings/change-phone.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { ChangePhoneScreen } from 'app/features/settings/changePhone' -import { HomeLayout } from 'app/features/home/layout.web' -import Head from 'next/head' -import { userProtectedGetSSP } from 'utils/userProtected' -import { NextPageWithLayout } from '../_app' - -export const Page: NextPageWithLayout = () => { - return ( - <> - - Change Phone - - - - - ) -} - -export const getServerSideProps = userProtectedGetSSP() -Page.getLayout = (children) => {children} - -export default Page diff --git a/apps/next/pages/settings/profile.tsx b/apps/next/pages/settings/profile.tsx deleted file mode 100644 index f21e9a8e3..000000000 --- a/apps/next/pages/settings/profile.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { EditProfile } from 'app/features/settings/components/editProfile/screen' -import { HomeLayout } from 'app/features/home/layout.web' -import Head from 'next/head' -import { userProtectedGetSSP } from 'utils/userProtected' -import { NextPageWithLayout } from '../_app' - -export const Page: NextPageWithLayout = () => { - return ( - <> - - Account - - - - - ) -} - -export const getServerSideProps = userProtectedGetSSP() -Page.getLayout = (children) => {children} - -export default Page diff --git a/apps/next/public/favicon-dark/android-chrome-192x192.png b/apps/next/public/favicon-dark/android-chrome-192x192.png deleted file mode 100644 index d9e9c27d3..000000000 Binary files a/apps/next/public/favicon-dark/android-chrome-192x192.png and /dev/null differ diff --git a/apps/next/public/favicon-dark/apple-touch-icon.png b/apps/next/public/favicon-dark/apple-touch-icon.png deleted file mode 100644 index e5bd97553..000000000 Binary files a/apps/next/public/favicon-dark/apple-touch-icon.png and /dev/null differ diff --git a/apps/next/public/favicon-dark/favicon-16x16.png b/apps/next/public/favicon-dark/favicon-16x16.png deleted file mode 100644 index e5143bbdd..000000000 Binary files a/apps/next/public/favicon-dark/favicon-16x16.png and /dev/null differ diff --git a/apps/next/public/favicon-dark/favicon-32x32.png b/apps/next/public/favicon-dark/favicon-32x32.png deleted file mode 100644 index 877429ff4..000000000 Binary files a/apps/next/public/favicon-dark/favicon-32x32.png and /dev/null differ diff --git a/apps/next/public/favicon-dark/favicon.ico b/apps/next/public/favicon-dark/favicon.ico deleted file mode 100644 index 4764d39d7..000000000 Binary files a/apps/next/public/favicon-dark/favicon.ico and /dev/null differ diff --git a/apps/next/public/favicon-dark/mstile-150x150.png b/apps/next/public/favicon-dark/mstile-150x150.png deleted file mode 100644 index 7dc8598d3..000000000 Binary files a/apps/next/public/favicon-dark/mstile-150x150.png and /dev/null differ diff --git a/apps/next/public/favicon-dark/safari-pinned-tab.svg b/apps/next/public/favicon-dark/safari-pinned-tab.svg deleted file mode 100644 index 3b8e834e5..000000000 --- a/apps/next/public/favicon-dark/safari-pinned-tab.svg +++ /dev/null @@ -1,27 +0,0 @@ - - - - -Created by potrace 1.14, written by Peter Selinger 2001-2017 - - - - - - diff --git a/apps/next/public/favicon-dark/site.webmanifest b/apps/next/public/favicon-dark/site.webmanifest deleted file mode 100644 index 35ba29709..000000000 --- a/apps/next/public/favicon-dark/site.webmanifest +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "Send", - "short_name": "Send", - "description": "Infrastructure for Merchants and Stablecoin Transactions", - "icons": [ - { - "src": "/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - } - ], - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" -} diff --git a/apps/next/public/favicon-light/android-chrome-192x192.png b/apps/next/public/favicon-light/android-chrome-192x192.png deleted file mode 100644 index 8a081b0d8..000000000 Binary files a/apps/next/public/favicon-light/android-chrome-192x192.png and /dev/null differ diff --git a/apps/next/public/favicon-light/apple-touch-icon.png b/apps/next/public/favicon-light/apple-touch-icon.png deleted file mode 100644 index 9bcbd9c78..000000000 Binary files a/apps/next/public/favicon-light/apple-touch-icon.png and /dev/null differ diff --git a/apps/next/public/favicon-light/browserconfig.xml b/apps/next/public/favicon-light/browserconfig.xml deleted file mode 100644 index b3930d0f0..000000000 --- a/apps/next/public/favicon-light/browserconfig.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - #da532c - - - diff --git a/apps/next/public/favicon-light/favicon-16x16.png b/apps/next/public/favicon-light/favicon-16x16.png deleted file mode 100644 index 146379363..000000000 Binary files a/apps/next/public/favicon-light/favicon-16x16.png and /dev/null differ diff --git a/apps/next/public/favicon-light/favicon-32x32.png b/apps/next/public/favicon-light/favicon-32x32.png deleted file mode 100644 index dcf5d9f91..000000000 Binary files a/apps/next/public/favicon-light/favicon-32x32.png and /dev/null differ diff --git a/apps/next/public/favicon-light/favicon.ico b/apps/next/public/favicon-light/favicon.ico deleted file mode 100644 index 23fa5068f..000000000 Binary files a/apps/next/public/favicon-light/favicon.ico and /dev/null differ diff --git a/apps/next/public/favicon-light/mstile-150x150.png b/apps/next/public/favicon-light/mstile-150x150.png deleted file mode 100644 index 824b2e0c5..000000000 Binary files a/apps/next/public/favicon-light/mstile-150x150.png and /dev/null differ diff --git a/apps/next/public/favicon-light/safari-pinned-tab.svg b/apps/next/public/favicon-light/safari-pinned-tab.svg deleted file mode 100644 index cc8fcca10..000000000 --- a/apps/next/public/favicon-light/safari-pinned-tab.svg +++ /dev/null @@ -1,27 +0,0 @@ - - - - -Created by potrace 1.14, written by Peter Selinger 2001-2017 - - - - - - diff --git a/apps/next/public/favicon-light/site.webmanifest b/apps/next/public/favicon-light/site.webmanifest deleted file mode 100644 index 35ba29709..000000000 --- a/apps/next/public/favicon-light/site.webmanifest +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "Send", - "short_name": "Send", - "description": "Infrastructure for Merchants and Stablecoin Transactions", - "icons": [ - { - "src": "/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - } - ], - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" -} diff --git a/apps/next/public/favicon/android-chrome-192x192.png b/apps/next/public/favicon/android-chrome-192x192.png new file mode 100644 index 000000000..36f8935ef Binary files /dev/null and b/apps/next/public/favicon/android-chrome-192x192.png differ diff --git a/apps/next/public/favicon/android-chrome-512x512.png b/apps/next/public/favicon/android-chrome-512x512.png new file mode 100644 index 000000000..8adeeb5e7 Binary files /dev/null and b/apps/next/public/favicon/android-chrome-512x512.png differ diff --git a/apps/next/public/favicon/apple-touch-icon.png b/apps/next/public/favicon/apple-touch-icon.png new file mode 100644 index 000000000..72f504b53 Binary files /dev/null and b/apps/next/public/favicon/apple-touch-icon.png differ diff --git a/apps/next/public/favicon-dark/browserconfig.xml b/apps/next/public/favicon/browserconfig.xml similarity index 82% rename from apps/next/public/favicon-dark/browserconfig.xml rename to apps/next/public/favicon/browserconfig.xml index b3930d0f0..8de1e649f 100644 --- a/apps/next/public/favicon-dark/browserconfig.xml +++ b/apps/next/public/favicon/browserconfig.xml @@ -3,7 +3,7 @@ - #da532c + #122023 diff --git a/apps/next/public/favicon/favicon-16x16.png b/apps/next/public/favicon/favicon-16x16.png new file mode 100644 index 000000000..f6a22420c Binary files /dev/null and b/apps/next/public/favicon/favicon-16x16.png differ diff --git a/apps/next/public/favicon/favicon-32x32.png b/apps/next/public/favicon/favicon-32x32.png new file mode 100644 index 000000000..336094a86 Binary files /dev/null and b/apps/next/public/favicon/favicon-32x32.png differ diff --git a/apps/next/public/favicon/favicon.ico b/apps/next/public/favicon/favicon.ico new file mode 100644 index 000000000..6a52be298 Binary files /dev/null and b/apps/next/public/favicon/favicon.ico differ diff --git a/apps/next/public/favicon/mstile-144x144.png b/apps/next/public/favicon/mstile-144x144.png new file mode 100644 index 000000000..221f73f4c Binary files /dev/null and b/apps/next/public/favicon/mstile-144x144.png differ diff --git a/apps/next/public/favicon/mstile-150x150.png b/apps/next/public/favicon/mstile-150x150.png new file mode 100644 index 000000000..fbfaf7ad4 Binary files /dev/null and b/apps/next/public/favicon/mstile-150x150.png differ diff --git a/apps/next/public/favicon/mstile-310x150.png b/apps/next/public/favicon/mstile-310x150.png new file mode 100644 index 000000000..42a438b1b Binary files /dev/null and b/apps/next/public/favicon/mstile-310x150.png differ diff --git a/apps/next/public/favicon/mstile-310x310.png b/apps/next/public/favicon/mstile-310x310.png new file mode 100644 index 000000000..b1eaaca6e Binary files /dev/null and b/apps/next/public/favicon/mstile-310x310.png differ diff --git a/apps/next/public/favicon/mstile-70x70.png b/apps/next/public/favicon/mstile-70x70.png new file mode 100644 index 000000000..dd333e123 Binary files /dev/null and b/apps/next/public/favicon/mstile-70x70.png differ diff --git a/apps/next/public/favicon/safari-pinned-tab.svg b/apps/next/public/favicon/safari-pinned-tab.svg new file mode 100644 index 000000000..255d9cf92 --- /dev/null +++ b/apps/next/public/favicon/safari-pinned-tab.svg @@ -0,0 +1,25 @@ + + + + +Created by potrace 1.14, written by Peter Selinger 2001-2017 + + + + + + diff --git a/apps/next/public/favicon/site.webmanifest b/apps/next/public/favicon/site.webmanifest new file mode 100644 index 000000000..a8435e9ac --- /dev/null +++ b/apps/next/public/favicon/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "Send", + "short_name": "Send", + "icons": [ + { + "src": "/favicon/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/favicon/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#122023", + "background_color": "#122023", + "display": "standalone" +} diff --git a/apps/next/public/tamagui.css b/apps/next/public/tamagui.css index 22a961608..fd3c1fb3a 100644 --- a/apps/next/public/tamagui.css +++ b/apps/next/public/tamagui.css @@ -1,9 +1,10 @@ ._ovs-contain {overscroll-behavior:contain;} .is_Text .is_Text {display:inline-flex;} ._dsp_contents {display:contents;} -:root {--color-1:hsl(206, 100%, 99.2%);--color-2:hsl(210, 100%, 98.0%);--color-3:hsl(209, 100%, 96.5%);--color-4:hsl(210, 98.8%, 94.0%);--color-5:hsl(209, 95.0%, 90.1%);--color-6:hsl(209, 81.2%, 84.5%);--color-7:hsl(208, 77.5%, 76.9%);--color-8:hsl(206, 81.9%, 65.3%);--color-9:hsl(206, 100%, 50.0%);--color-10:hsl(208, 100%, 47.3%);--color-11:hsl(211, 100%, 43.2%);--color-12:hsl(211, 100%, 15.0%);--color-13:hsl(0, 0%, 99.0%);--color-14:hsl(0, 0%, 97.3%);--color-15:hsl(0, 0%, 95.1%);--color-16:hsl(0, 0%, 93.0%);--color-17:hsl(0, 0%, 90.9%);--color-18:hsl(0, 0%, 88.7%);--color-19:hsl(0, 0%, 85.8%);--color-20:hsl(0, 0%, 78.0%);--color-21:hsl(0, 0%, 56.1%);--color-22:hsl(0, 0%, 52.3%);--color-23:hsl(0, 0%, 43.5%);--color-24:hsl(0, 0%, 9.0%);--color-25:hsl(136, 50.0%, 98.9%);--color-26:hsl(138, 62.5%, 96.9%);--color-27:hsl(139, 55.2%, 94.5%);--color-28:hsl(140, 48.7%, 91.0%);--color-29:hsl(141, 43.7%, 86.0%);--color-30:hsl(143, 40.3%, 79.0%);--color-31:hsl(146, 38.5%, 69.0%);--color-32:hsl(151, 40.2%, 54.1%);--color-33:hsl(151, 55.0%, 41.5%);--color-34:hsl(152, 57.5%, 37.6%);--color-35:hsl(153, 67.0%, 28.5%);--color-36:hsl(155, 40.0%, 14.0%);--color-37:hsl(24, 70.0%, 99.0%);--color-38:hsl(24, 83.3%, 97.6%);--color-39:hsl(24, 100%, 95.3%);--color-40:hsl(25, 100%, 92.2%);--color-41:hsl(25, 100%, 88.2%);--color-42:hsl(25, 100%, 82.8%);--color-43:hsl(24, 100%, 75.3%);--color-44:hsl(24, 94.5%, 64.3%);--color-45:hsl(24, 94.0%, 50.0%);--color-46:hsl(24, 100%, 46.5%);--color-47:hsl(24, 100%, 37.0%);--color-48:hsl(15, 60.0%, 17.0%);--color-49:hsl(322, 100%, 99.4%);--color-50:hsl(323, 100%, 98.4%);--color-51:hsl(323, 86.3%, 96.5%);--color-52:hsl(323, 78.7%, 94.2%);--color-53:hsl(323, 72.2%, 91.1%);--color-54:hsl(323, 66.3%, 86.6%);--color-55:hsl(323, 62.0%, 80.1%);--color-56:hsl(323, 60.3%, 72.4%);--color-57:hsl(322, 65.0%, 54.5%);--color-58:hsl(322, 63.9%, 50.7%);--color-59:hsl(322, 75.0%, 46.0%);--color-60:hsl(320, 70.0%, 13.5%);--color-61:hsl(280, 65.0%, 99.4%);--color-62:hsl(276, 100%, 99.0%);--color-63:hsl(276, 83.1%, 97.0%);--color-64:hsl(275, 76.4%, 94.7%);--color-65:hsl(275, 70.8%, 91.8%);--color-66:hsl(274, 65.4%, 87.8%);--color-67:hsl(273, 61.0%, 81.7%);--color-68:hsl(272, 60.0%, 73.5%);--color-69:hsl(272, 51.0%, 54.0%);--color-70:hsl(272, 46.8%, 50.3%);--color-71:hsl(272, 50.0%, 45.8%);--color-72:hsl(272, 66.0%, 16.0%);--color-73:hsl(359, 100%, 99.4%);--color-74:hsl(359, 100%, 98.6%);--color-75:hsl(360, 100%, 96.8%);--color-76:hsl(360, 97.9%, 94.8%);--color-77:hsl(360, 90.2%, 91.9%);--color-78:hsl(360, 81.7%, 87.8%);--color-79:hsl(359, 74.2%, 81.7%);--color-80:hsl(359, 69.5%, 74.3%);--color-81:hsl(358, 75.0%, 59.0%);--color-82:hsl(358, 69.4%, 55.2%);--color-83:hsl(358, 65.0%, 48.7%);--color-84:hsl(354, 50.0%, 14.6%);--color-85:hsl(60, 54.0%, 98.5%);--color-86:hsl(52, 100%, 95.5%);--color-87:hsl(55, 100%, 90.9%);--color-88:hsl(54, 100%, 86.6%);--color-89:hsl(52, 97.9%, 82.0%);--color-90:hsl(50, 89.4%, 76.1%);--color-91:hsl(47, 80.4%, 68.0%);--color-92:hsl(48, 100%, 46.1%);--color-93:hsl(53, 92.0%, 50.0%);--color-94:hsl(50, 100%, 48.5%);--color-95:hsl(42, 100%, 29.0%);--color-96:hsl(40, 55.0%, 13.5%);--color-97:hsl(50, 20.0%, 99.1%);--color-98:hsl(47, 52.9%, 96.7%);--color-99:hsl(46, 38.2%, 93.7%);--color-100:hsl(44, 32.7%, 90.1%);--color-101:hsl(43, 29.9%, 85.7%);--color-102:hsl(41, 28.3%, 79.8%);--color-103:hsl(39, 27.6%, 71.9%);--color-104:hsl(36, 27.2%, 61.8%);--color-105:hsl(36, 20.0%, 49.5%);--color-106:hsl(36, 19.8%, 45.7%);--color-107:hsl(36, 20.0%, 39.0%);--color-108:hsl(36, 16.0%, 20.0%);--color-109:hsla(125, 96%, 40%, 0.5);--color-110:hsla(125, 96%, 40%, 0.75);--color-111:hsla(125, 96%, 40%, 1);--color-112:hsla(125, 96%, 42%, 1);--color-113:hsla(125, 96%, 45%, 1);--color-114:hsla(125, 96%, 47%, 1);--color-115:hsla(125, 96%, 50%, 1);--color-116:hsla(125, 96%, 52%, 1);--color-117:hsla(125, 96%, 55%, 1);--color-118:hsla(125, 96%, 57%, 1);--color-119:hsla(125, 96%, 59%, 1);--color-120:hsla(125, 96%, 62%, 1);--color-121:hsl(212, 35.0%, 9.2%);--color-122:hsl(216, 50.0%, 11.8%);--color-123:hsl(214, 59.4%, 15.3%);--color-124:hsl(214, 65.8%, 17.9%);--color-125:hsl(213, 71.2%, 20.2%);--color-126:hsl(212, 77.4%, 23.1%);--color-127:hsl(211, 85.1%, 27.4%);--color-128:hsl(211, 89.7%, 34.1%);--color-129:hsl(206, 100%, 50.0%);--color-130:hsl(209, 100%, 60.6%);--color-131:hsl(210, 100%, 66.1%);--color-132:hsl(206, 98.0%, 95.8%);--color-133:hsl(0, 0%, 8.5%);--color-134:hsl(0, 0%, 11.0%);--color-135:hsl(0, 0%, 13.6%);--color-136:hsl(0, 0%, 15.8%);--color-137:hsl(0, 0%, 17.9%);--color-138:hsl(0, 0%, 20.5%);--color-139:hsl(0, 0%, 24.3%);--color-140:hsl(0, 0%, 31.2%);--color-141:hsl(0, 0%, 43.9%);--color-142:hsl(0, 0%, 49.4%);--color-143:hsl(0, 0%, 62.8%);--color-144:hsl(0, 0%, 93.0%);--color-145:hsl(146, 30.0%, 7.4%);--color-146:hsl(155, 44.2%, 8.4%);--color-147:hsl(155, 46.7%, 10.9%);--color-148:hsl(154, 48.4%, 12.9%);--color-149:hsl(154, 49.7%, 14.9%);--color-150:hsl(154, 50.9%, 17.6%);--color-151:hsl(153, 51.8%, 21.8%);--color-152:hsl(151, 51.7%, 28.4%);--color-153:hsl(151, 55.0%, 41.5%);--color-154:hsl(151, 49.3%, 46.5%);--color-155:hsl(151, 50.0%, 53.2%);--color-156:hsl(137, 72.0%, 94.0%);--color-157:hsl(30, 70.0%, 7.2%);--color-158:hsl(28, 100%, 8.4%);--color-159:hsl(26, 91.1%, 11.6%);--color-160:hsl(25, 88.3%, 14.1%);--color-161:hsl(24, 87.6%, 16.6%);--color-162:hsl(24, 88.6%, 19.8%);--color-163:hsl(24, 92.4%, 24.0%);--color-164:hsl(25, 100%, 29.0%);--color-165:hsl(24, 94.0%, 50.0%);--color-166:hsl(24, 100%, 58.5%);--color-167:hsl(24, 100%, 62.2%);--color-168:hsl(24, 97.0%, 93.2%);--color-169:hsl(318, 25.0%, 9.6%);--color-170:hsl(319, 32.2%, 11.6%);--color-171:hsl(319, 41.0%, 16.0%);--color-172:hsl(320, 45.4%, 18.7%);--color-173:hsl(320, 49.0%, 21.1%);--color-174:hsl(321, 53.6%, 24.4%);--color-175:hsl(321, 61.1%, 29.7%);--color-176:hsl(322, 74.9%, 37.5%);--color-177:hsl(322, 65.0%, 54.5%);--color-178:hsl(323, 72.8%, 59.2%);--color-179:hsl(325, 90.0%, 66.4%);--color-180:hsl(322, 90.0%, 95.8%);--color-181:hsl(284, 20.0%, 9.6%);--color-182:hsl(283, 30.0%, 11.8%);--color-183:hsl(281, 37.5%, 16.5%);--color-184:hsl(280, 41.2%, 20.0%);--color-185:hsl(279, 43.8%, 23.3%);--color-186:hsl(277, 46.4%, 27.5%);--color-187:hsl(275, 49.3%, 34.6%);--color-188:hsl(272, 52.1%, 45.9%);--color-189:hsl(272, 51.0%, 54.0%);--color-190:hsl(273, 57.3%, 59.1%);--color-191:hsl(275, 80.0%, 71.0%);--color-192:hsl(279, 75.0%, 95.7%);--color-193:hsl(353, 23.0%, 9.8%);--color-194:hsl(357, 34.4%, 12.0%);--color-195:hsl(356, 43.4%, 16.4%);--color-196:hsl(356, 47.6%, 19.2%);--color-197:hsl(356, 51.1%, 21.9%);--color-198:hsl(356, 55.2%, 25.9%);--color-199:hsl(357, 60.2%, 31.8%);--color-200:hsl(358, 65.0%, 40.4%);--color-201:hsl(358, 75.0%, 59.0%);--color-202:hsl(358, 85.3%, 64.0%);--color-203:hsl(358, 100%, 69.5%);--color-204:hsl(351, 89.0%, 96.0%);--color-205:hsl(45, 100%, 5.5%);--color-206:hsl(46, 100%, 6.7%);--color-207:hsl(45, 100%, 8.7%);--color-208:hsl(45, 100%, 10.4%);--color-209:hsl(47, 100%, 12.1%);--color-210:hsl(49, 100%, 14.3%);--color-211:hsl(49, 90.3%, 18.4%);--color-212:hsl(50, 100%, 22.0%);--color-213:hsl(53, 92.0%, 50.0%);--color-214:hsl(54, 100%, 68.0%);--color-215:hsl(48, 100%, 47.0%);--color-216:hsl(53, 100%, 91.0%);--color-217:hsl(44, 9.0%, 8.3%);--color-218:hsl(43, 14.3%, 9.6%);--color-219:hsl(42, 15.5%, 13.0%);--color-220:hsl(41, 16.4%, 15.6%);--color-221:hsl(41, 16.9%, 17.8%);--color-222:hsl(40, 17.6%, 20.8%);--color-223:hsl(38, 18.5%, 26.4%);--color-224:hsl(36, 19.6%, 35.1%);--color-225:hsl(36, 20.0%, 49.5%);--color-226:hsl(36, 22.3%, 54.5%);--color-227:hsl(35, 30.0%, 64.0%);--color-228:hsl(49, 52.0%, 93.8%);--color-229:hsla(125, 96%, 40%, 0.5);--color-230:hsla(125, 96%, 40%, 0.75);--color-231:hsla(125, 96%, 40%, 1);--color-232:hsla(125, 96%, 42%, 1);--color-233:hsla(125, 96%, 45%, 1);--color-234:hsla(125, 96%, 47%, 1);--color-235:hsla(125, 96%, 50%, 1);--color-236:hsla(125, 96%, 52%, 1);--color-237:hsla(125, 96%, 55%, 1);--color-238:hsla(125, 96%, 57%, 1);--color-239:hsla(125, 96%, 59%, 1);--color-240:hsla(125, 96%, 62%, 1);--color-241:#40FB50;--color-242:#FFFFFF;--color-243:#000000;--color-244:#F5F6FC;--color-245:#E8ECFB;--color-246:#D2D9EE;--color-247:#B8C0DC;--color-248:#A6AFCA;--color-249:#98A1C0;--color-250:#888FAB;--color-251:#7780A0;--color-252:#6B7594;--color-253:#5D6785;--color-254:#505A78;--color-255:#404A67;--color-256:#333D59;--color-257:#293249;--color-258:#1B2236;--color-259:#131A2A;--color-260:#0E1524;--color-261:#0D111C;--color-262:#FFF2F7;--color-263:#FFD9E4;--color-264:#FBA4C0;--color-265:#FF6FA3;--color-266:#FB118E;--color-267:#C41A69;--color-268:#8C0F49;--color-269:#55072A;--color-270:#39061B;--color-271:#2B000B;--color-272:#F51A70;--color-273:#FEF0EE;--color-274:#FED5CF;--color-275:#FEA79B;--color-276:#FD766B;--color-277:#FA2B39;--color-278:#C4292F;--color-279:#891E20;--color-280:#530F0F;--color-281:#380A03;--color-282:#240800;--color-283:#F14544;--color-284:#FEF8C4;--color-285:#F0E49A;--color-286:#DBBC19;--color-287:#BB9F13;--color-288:#A08116;--color-289:#866311;--color-290:#5D4204;--color-291:#3E2B04;--color-292:#231902;--color-293:#180F02;--color-294:#FAF40A;--color-295:#FFF5E8;--color-296:#F8DEB6;--color-297:#EEB317;--color-298:#DB900B;--color-299:#B17900;--color-300:#905C10;--color-301:#643F07;--color-302:#3F2208;--color-303:#29160F;--color-304:#161007;--color-305:#FEB239;--color-306:#EDFDF0;--color-307:#BFEECA;--color-308:#76D191;--color-309:#40B66B;--color-310:#209853;--color-311:#0B783E;--color-312:#0C522A;--color-313:#053117;--color-314:#091F10;--color-315:#09130B;--color-316:#5CFE9D;--color-317:#F3F5FE;--color-318:#DEE1FF;--color-319:#ADBCFF;--color-320:#869EFF;--color-321:#4C82FB;--color-322:#1267D6;--color-323:#1D4294;--color-324:#09265E;--color-325:#0B193F;--color-326:#040E34;--color-327:#587BFF;--color-328:#F2FEDB;--color-329:#D3EBA3;--color-330:#9BCD46;--color-331:#7BB10C;--color-332:#649205;--color-333:#527318;--color-334:#344F00;--color-335:#233401;--color-336:#171D00;--color-337:#0E1300;--color-338:#B1F13C;--color-339:#FEEDE5;--color-340:#FCD9C8;--color-341:#FBAA7F;--color-342:#F67E3E;--color-343:#DC5B14;--color-344:#AF460A;--color-345:#76330F;--color-346:#4D220B;--color-347:#2A1505;--color-348:#1C0E03;--color-349:#FF6F1E;--color-350:#FFF1FE;--color-351:#FAD8F8;--color-352:#F5A1F5;--color-353:#F06DF3;--color-354:#DC39E3;--color-355:#AF2EB4;--color-356:#7A1C7D;--color-357:#550D56;--color-358:#330733;--color-359:#250225;--color-360:#FC72FF;--color-361:#F1EFFE;--color-362:#E2DEFD;--color-363:#BDB8FA;--color-364:#9D99F5;--color-365:#7A7BEB;--color-366:#515EDC;--color-367:#343F9E;--color-368:#232969;--color-369:#121643;--color-370:#0E0D30;--color-371:#5065FD;--color-372:#D6F5FE;--color-373:#B0EDFE;--color-374:#63CDE8;--color-375:#2FB0CC;--color-376:#2092AB;--color-377:#117489;--color-378:#014F5F;--color-379:#003540;--color-380:#011E26;--color-381:#011418;--color-382:#36DBFF;--color-383:#F1FCEF;--color-384:#DAE6D8;--color-385:#B8C3B7;--color-386:#9AA498;--color-387:#7E887D;--color-388:#646B62;--color-389:#434942;--color-390:#2C302C;--color-391:#181B18;--color-392:#0F120E;--color-393:#7E887D;--color-394:#393939;--color-395:#e6e6e6;--color-396:#FA2B39;--color-397:#a26af3;--color-398:#28A0F0;--color-399:#2151F5;--color-400:#F0B90B;--color-401:#FB36D0;--color-402:#9f7750;--color-403:#C3B29E;--color-404:#1D1D20;--color-405:#86AE80;--radius-1:0px;--radius-2:3px;--radius-3:5px;--radius-4:7px;--radius-5:9px;--radius-6:10px;--radius-7:16px;--radius-8:19px;--radius-9:22px;--radius-10:26px;--radius-11:34px;--radius-12:42px;--radius-13:50px;--radius-14:16px;--zIndex-1:0;--zIndex-2:100;--zIndex-3:200;--zIndex-4:300;--zIndex-5:400;--zIndex-6:500;--space-1:0px;--space-6:2px;--space-8:7px;--space-10:13px;--space-12:18px;--space-15:24px;--space-16:32px;--space-17:39px;--space-18:46px;--space-19:53px;--space-20:60px;--space-21:74px;--space-22:88px;--space-23:102px;--space-24:116px;--space-25:130px;--space-26:144px;--space-27:144px;--space-28:158px;--space-29:172px;--space-30:186px;--space-31:249px;--space-32:284px;--space-2:0.5px;--space-3:1px;--space-4:1.5px;--space-5:5px;--space-7:4px;--space-9:10px;--space-11:16px;--space-13:18px;--space-14:21px;--space-33:-0.5px;--space-34:-1px;--space-35:-1.5px;--space-36:-5px;--space-37:-2px;--space-38:-4px;--space-39:-7px;--space-40:-10px;--space-41:-13px;--space-42:-16px;--space-43:-18px;--space-44:-18px;--space-45:-21px;--space-46:-24px;--space-47:-32px;--space-48:-39px;--space-49:-46px;--space-50:-53px;--space-51:-60px;--space-52:-74px;--space-53:-88px;--space-54:-102px;--space-55:-116px;--space-56:-130px;--space-57:-144px;--space-58:-144px;--space-59:-158px;--space-60:-172px;--space-61:-186px;--space-62:-249px;--space-63:-284px;--size-1:0px;--size-6:20px;--size-8:28px;--size-10:36px;--size-12:44px;--size-15:52px;--size-16:64px;--size-17:74px;--size-18:84px;--size-19:94px;--size-20:104px;--size-21:124px;--size-22:144px;--size-23:164px;--size-24:184px;--size-25:204px;--size-26:224px;--size-27:224px;--size-28:244px;--size-29:264px;--size-30:284px;--size-31:374px;--size-32:424px;--size-2:2px;--size-3:4px;--size-4:8px;--size-5:16px;--size-7:24px;--size-9:32px;--size-11:40px;--size-13:44px;--size-14:48px} -:root .font_heading, :root .t_lang-heading-default .font_heading {--f-fa:Inter, -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;--f-li-1:21px;--f-li-2:22px;--f-li-3:23px;--f-li-4:24px;--f-li-5:26px;--f-li-6:25px;--f-li-7:30px;--f-li-8:33px;--f-li-9:40px;--f-li-10:56px;--f-li-11:65px;--f-li-12:72px;--f-li-13:82px;--f-li-14:102px;--f-li-15:111px;--f-li-16:144px;--f-li-17:24px;--f-we-1:400;--f-we-2:400;--f-we-3:400;--f-we-4:400;--f-we-5:400;--f-we-6:400;--f-we-7:700;--f-we-8:700;--f-we-9:700;--f-we-10:700;--f-we-11:700;--f-we-12:700;--f-we-13:700;--f-we-14:700;--f-we-15:700;--f-we-16:700;--f-we-17:700;--f-21-1:2px;--f-21-2:2px;--f-21-3:2px;--f-21-4:2px;--f-21-5:2px;--f-21-6:1px;--f-21-7:0px;--f-21-8:-1px;--f-21-9:-2px;--f-21-10:-3px;--f-21-11:-3px;--f-21-12:-4px;--f-21-13:-4px;--f-21-14:-5px;--f-21-15:-6px;--f-21-16:-6px;--f-21-17:-6px;--f-si-1:11px;--f-si-2:12px;--f-si-3:13px;--f-si-4:14px;--f-si-5:16px;--f-si-6:15px;--f-si-7:20px;--f-si-8:23px;--f-si-9:30px;--f-si-10:46px;--f-si-11:55px;--f-si-12:62px;--f-si-13:72px;--f-si-14:92px;--f-si-15:101px;--f-si-16:134px;--f-si-17:14px;--f-tr-1:uppercase;--f-tr-2:uppercase;--f-tr-3:uppercase;--f-tr-4:uppercase;--f-tr-5:uppercase;--f-tr-6:uppercase;--f-tr-7:none;--f-tr-8:none;--f-tr-9:none;--f-tr-10:none;--f-tr-11:none;--f-tr-12:none;--f-tr-13:none;--f-tr-14:none;--f-tr-15:none;--f-tr-16:none;--f-tr-17:none} -:root .font_body, :root .t_lang-body-default .font_body {--f-fa:Inter, -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;--f-li-1:23px;--f-li-2:24px;--f-li-3:25px;--f-li-4:27px;--f-li-5:30px;--f-li-6:32px;--f-li-7:34px;--f-li-8:38px;--f-li-9:46px;--f-li-10:66px;--f-li-11:77px;--f-li-12:85px;--f-li-13:97px;--f-li-14:121px;--f-li-15:148px;--f-li-16:172px;--f-li-17:27px;--f-we-1:300;--f-we-2:300;--f-we-3:300;--f-we-4:300;--f-we-5:300;--f-we-6:300;--f-we-7:300;--f-we-8:300;--f-we-9:300;--f-we-10:300;--f-we-11:300;--f-we-12:300;--f-we-13:300;--f-we-14:300;--f-we-15:300;--f-we-16:300;--f-we-17:300;--f-21-1:0px;--f-21-2:0px;--f-21-3:0px;--f-21-4:0px;--f-21-5:0px;--f-21-6:0px;--f-21-7:0px;--f-21-8:0px;--f-21-9:0px;--f-21-10:0px;--f-21-11:0px;--f-21-12:0px;--f-21-13:0px;--f-21-14:0px;--f-21-15:0px;--f-21-16:0px;--f-21-17:0px;--f-si-1:12px;--f-si-2:13px;--f-si-3:14px;--f-si-4:15px;--f-si-5:18px;--f-si-6:20px;--f-si-7:22px;--f-si-8:25px;--f-si-9:33px;--f-si-10:51px;--f-si-11:61px;--f-si-12:68px;--f-si-13:79px;--f-si-14:101px;--f-si-15:125px;--f-si-16:147px;--f-si-17:15px} +:root {--color-1:hsl(206, 100%, 99.2%);--color-2:hsl(210, 100%, 98.0%);--color-3:hsl(209, 100%, 96.5%);--color-4:hsl(210, 98.8%, 94.0%);--color-5:hsl(209, 95.0%, 90.1%);--color-6:hsl(209, 81.2%, 84.5%);--color-7:hsl(208, 77.5%, 76.9%);--color-8:hsl(206, 81.9%, 65.3%);--color-9:hsl(206, 100%, 50.0%);--color-10:hsl(208, 100%, 47.3%);--color-11:hsl(211, 100%, 43.2%);--color-12:hsl(211, 100%, 15.0%);--color-13:hsl(0, 0%, 99.0%);--color-14:hsl(0, 0%, 97.3%);--color-15:hsl(0, 0%, 95.1%);--color-16:hsl(0, 0%, 93.0%);--color-17:hsl(0, 0%, 90.9%);--color-18:hsl(0, 0%, 88.7%);--color-19:hsl(0, 0%, 85.8%);--color-20:hsl(0, 0%, 78.0%);--color-21:hsl(0, 0%, 56.1%);--color-22:hsl(0, 0%, 52.3%);--color-23:hsl(0, 0%, 43.5%);--color-24:hsl(0, 0%, 9.0%);--color-25:hsl(136, 50.0%, 98.9%);--color-26:hsl(138, 62.5%, 96.9%);--color-27:hsl(139, 55.2%, 94.5%);--color-28:hsl(140, 48.7%, 91.0%);--color-29:hsl(141, 43.7%, 86.0%);--color-30:hsl(143, 40.3%, 79.0%);--color-31:hsl(146, 38.5%, 69.0%);--color-32:hsl(151, 40.2%, 54.1%);--color-33:hsl(151, 55.0%, 41.5%);--color-34:hsl(152, 57.5%, 37.6%);--color-35:hsl(153, 67.0%, 28.5%);--color-36:hsl(155, 40.0%, 14.0%);--color-37:hsl(24, 70.0%, 99.0%);--color-38:hsl(24, 83.3%, 97.6%);--color-39:hsl(24, 100%, 95.3%);--color-40:hsl(25, 100%, 92.2%);--color-41:hsl(25, 100%, 88.2%);--color-42:hsl(25, 100%, 82.8%);--color-43:hsl(24, 100%, 75.3%);--color-44:hsl(24, 94.5%, 64.3%);--color-45:hsl(24, 94.0%, 50.0%);--color-46:hsl(24, 100%, 46.5%);--color-47:hsl(24, 100%, 37.0%);--color-48:hsl(15, 60.0%, 17.0%);--color-49:hsl(322, 100%, 99.4%);--color-50:hsl(323, 100%, 98.4%);--color-51:hsl(323, 86.3%, 96.5%);--color-52:hsl(323, 78.7%, 94.2%);--color-53:hsl(323, 72.2%, 91.1%);--color-54:hsl(323, 66.3%, 86.6%);--color-55:hsl(323, 62.0%, 80.1%);--color-56:hsl(323, 60.3%, 72.4%);--color-57:hsl(322, 65.0%, 54.5%);--color-58:hsl(322, 63.9%, 50.7%);--color-59:hsl(322, 75.0%, 46.0%);--color-60:hsl(320, 70.0%, 13.5%);--color-61:hsl(280, 65.0%, 99.4%);--color-62:hsl(276, 100%, 99.0%);--color-63:hsl(276, 83.1%, 97.0%);--color-64:hsl(275, 76.4%, 94.7%);--color-65:hsl(275, 70.8%, 91.8%);--color-66:hsl(274, 65.4%, 87.8%);--color-67:hsl(273, 61.0%, 81.7%);--color-68:hsl(272, 60.0%, 73.5%);--color-69:hsl(272, 51.0%, 54.0%);--color-70:hsl(272, 46.8%, 50.3%);--color-71:hsl(272, 50.0%, 45.8%);--color-72:hsl(272, 66.0%, 16.0%);--color-73:hsl(359, 100%, 99.4%);--color-74:hsl(359, 100%, 98.6%);--color-75:hsl(360, 100%, 96.8%);--color-76:hsl(360, 97.9%, 94.8%);--color-77:hsl(360, 90.2%, 91.9%);--color-78:hsl(360, 81.7%, 87.8%);--color-79:hsl(359, 74.2%, 81.7%);--color-80:hsl(359, 69.5%, 74.3%);--color-81:hsl(358, 75.0%, 59.0%);--color-82:hsl(358, 69.4%, 55.2%);--color-83:hsl(358, 65.0%, 48.7%);--color-84:hsl(354, 50.0%, 14.6%);--color-85:hsl(60, 54.0%, 98.5%);--color-86:hsl(52, 100%, 95.5%);--color-87:hsl(55, 100%, 90.9%);--color-88:hsl(54, 100%, 86.6%);--color-89:hsl(52, 97.9%, 82.0%);--color-90:hsl(50, 89.4%, 76.1%);--color-91:hsl(47, 80.4%, 68.0%);--color-92:hsl(48, 100%, 46.1%);--color-93:hsl(53, 92.0%, 50.0%);--color-94:hsl(50, 100%, 48.5%);--color-95:hsl(42, 100%, 29.0%);--color-96:hsl(40, 55.0%, 13.5%);--color-97:hsl(50, 20.0%, 99.1%);--color-98:hsl(47, 52.9%, 96.7%);--color-99:hsl(46, 38.2%, 93.7%);--color-100:hsl(44, 32.7%, 90.1%);--color-101:hsl(43, 29.9%, 85.7%);--color-102:hsl(41, 28.3%, 79.8%);--color-103:hsl(39, 27.6%, 71.9%);--color-104:hsl(36, 27.2%, 61.8%);--color-105:hsl(36, 20.0%, 49.5%);--color-106:hsl(36, 19.8%, 45.7%);--color-107:hsl(36, 20.0%, 39.0%);--color-108:hsl(36, 16.0%, 20.0%);--color-109:hsla(125, 96%, 40%, 0.5);--color-110:hsla(125, 96%, 40%, 0.75);--color-111:hsla(125, 96%, 40%, 1);--color-112:hsla(125, 96%, 42%, 1);--color-113:hsla(125, 96%, 45%, 1);--color-114:hsla(125, 96%, 47%, 1);--color-115:hsla(125, 96%, 50%, 1);--color-116:hsla(125, 96%, 52%, 1);--color-117:hsla(125, 96%, 55%, 1);--color-118:hsla(125, 96%, 57%, 1);--color-119:hsla(125, 96%, 59%, 1);--color-120:hsla(125, 96%, 62%, 1);--color-121:hsl(212, 35.0%, 9.2%);--color-122:hsl(216, 50.0%, 11.8%);--color-123:hsl(214, 59.4%, 15.3%);--color-124:hsl(214, 65.8%, 17.9%);--color-125:hsl(213, 71.2%, 20.2%);--color-126:hsl(212, 77.4%, 23.1%);--color-127:hsl(211, 85.1%, 27.4%);--color-128:hsl(211, 89.7%, 34.1%);--color-129:hsl(206, 100%, 50.0%);--color-130:hsl(209, 100%, 60.6%);--color-131:hsl(210, 100%, 66.1%);--color-132:hsl(206, 98.0%, 95.8%);--color-133:hsl(0, 0%, 8.5%);--color-134:hsl(0, 0%, 11.0%);--color-135:hsl(0, 0%, 13.6%);--color-136:hsl(0, 0%, 15.8%);--color-137:hsl(0, 0%, 17.9%);--color-138:hsl(0, 0%, 20.5%);--color-139:hsl(0, 0%, 24.3%);--color-140:hsl(0, 0%, 31.2%);--color-141:hsl(0, 0%, 43.9%);--color-142:hsl(0, 0%, 49.4%);--color-143:hsl(0, 0%, 62.8%);--color-144:hsl(0, 0%, 93.0%);--color-145:hsl(146, 30.0%, 7.4%);--color-146:hsl(155, 44.2%, 8.4%);--color-147:hsl(155, 46.7%, 10.9%);--color-148:hsl(154, 48.4%, 12.9%);--color-149:hsl(154, 49.7%, 14.9%);--color-150:hsl(154, 50.9%, 17.6%);--color-151:hsl(153, 51.8%, 21.8%);--color-152:hsl(151, 51.7%, 28.4%);--color-153:hsl(151, 55.0%, 41.5%);--color-154:hsl(151, 49.3%, 46.5%);--color-155:hsl(151, 50.0%, 53.2%);--color-156:hsl(137, 72.0%, 94.0%);--color-157:hsl(30, 70.0%, 7.2%);--color-158:hsl(28, 100%, 8.4%);--color-159:hsl(26, 91.1%, 11.6%);--color-160:hsl(25, 88.3%, 14.1%);--color-161:hsl(24, 87.6%, 16.6%);--color-162:hsl(24, 88.6%, 19.8%);--color-163:hsl(24, 92.4%, 24.0%);--color-164:hsl(25, 100%, 29.0%);--color-165:hsl(24, 94.0%, 50.0%);--color-166:hsl(24, 100%, 58.5%);--color-167:hsl(24, 100%, 62.2%);--color-168:hsl(24, 97.0%, 93.2%);--color-169:hsl(318, 25.0%, 9.6%);--color-170:hsl(319, 32.2%, 11.6%);--color-171:hsl(319, 41.0%, 16.0%);--color-172:hsl(320, 45.4%, 18.7%);--color-173:hsl(320, 49.0%, 21.1%);--color-174:hsl(321, 53.6%, 24.4%);--color-175:hsl(321, 61.1%, 29.7%);--color-176:hsl(322, 74.9%, 37.5%);--color-177:hsl(322, 65.0%, 54.5%);--color-178:hsl(323, 72.8%, 59.2%);--color-179:hsl(325, 90.0%, 66.4%);--color-180:hsl(322, 90.0%, 95.8%);--color-181:hsl(284, 20.0%, 9.6%);--color-182:hsl(283, 30.0%, 11.8%);--color-183:hsl(281, 37.5%, 16.5%);--color-184:hsl(280, 41.2%, 20.0%);--color-185:hsl(279, 43.8%, 23.3%);--color-186:hsl(277, 46.4%, 27.5%);--color-187:hsl(275, 49.3%, 34.6%);--color-188:hsl(272, 52.1%, 45.9%);--color-189:hsl(272, 51.0%, 54.0%);--color-190:hsl(273, 57.3%, 59.1%);--color-191:hsl(275, 80.0%, 71.0%);--color-192:hsl(279, 75.0%, 95.7%);--color-193:hsl(353, 23.0%, 9.8%);--color-194:hsl(357, 34.4%, 12.0%);--color-195:hsl(356, 43.4%, 16.4%);--color-196:hsl(356, 47.6%, 19.2%);--color-197:hsl(356, 51.1%, 21.9%);--color-198:hsl(356, 55.2%, 25.9%);--color-199:hsl(357, 60.2%, 31.8%);--color-200:hsl(358, 65.0%, 40.4%);--color-201:hsl(358, 75.0%, 59.0%);--color-202:hsl(358, 85.3%, 64.0%);--color-203:hsl(358, 100%, 69.5%);--color-204:hsl(351, 89.0%, 96.0%);--color-205:hsl(45, 100%, 5.5%);--color-206:hsl(46, 100%, 6.7%);--color-207:hsl(45, 100%, 8.7%);--color-208:hsl(45, 100%, 10.4%);--color-209:hsl(47, 100%, 12.1%);--color-210:hsl(49, 100%, 14.3%);--color-211:hsl(49, 90.3%, 18.4%);--color-212:hsl(50, 100%, 22.0%);--color-213:hsl(53, 92.0%, 50.0%);--color-214:hsl(54, 100%, 68.0%);--color-215:hsl(48, 100%, 47.0%);--color-216:hsl(53, 100%, 91.0%);--color-217:hsl(44, 9.0%, 8.3%);--color-218:hsl(43, 14.3%, 9.6%);--color-219:hsl(42, 15.5%, 13.0%);--color-220:hsl(41, 16.4%, 15.6%);--color-221:hsl(41, 16.9%, 17.8%);--color-222:hsl(40, 17.6%, 20.8%);--color-223:hsl(38, 18.5%, 26.4%);--color-224:hsl(36, 19.6%, 35.1%);--color-225:hsl(36, 20.0%, 49.5%);--color-226:hsl(36, 22.3%, 54.5%);--color-227:hsl(35, 30.0%, 64.0%);--color-228:hsl(49, 52.0%, 93.8%);--color-229:hsla(125, 96%, 40%, 0.5);--color-230:hsla(125, 96%, 40%, 0.75);--color-231:hsla(125, 96%, 40%, 1);--color-232:hsla(125, 96%, 42%, 1);--color-233:hsla(125, 96%, 45%, 1);--color-234:hsla(125, 96%, 47%, 1);--color-235:hsla(125, 96%, 50%, 1);--color-236:hsla(125, 96%, 52%, 1);--color-237:hsla(125, 96%, 55%, 1);--color-238:hsla(125, 96%, 57%, 1);--color-239:hsla(125, 96%, 59%, 1);--color-240:hsla(125, 96%, 62%, 1);--color-241:#40FB50;--color-242:#081619;--color-243:#FFFFFF;--color-244:#122023;--color-245:#F5F6FC;--color-246:#E8ECFB;--color-247:#D2D9EE;--color-248:#B8C0DC;--color-249:#A6AFCA;--color-250:#98A1C0;--color-251:#888FAB;--color-252:#7780A0;--color-253:#6B7594;--color-254:#5D6785;--color-255:#505A78;--color-256:#404A67;--color-257:#333D59;--color-258:#293249;--color-259:#1B2236;--color-260:#131A2A;--color-261:#0E1524;--color-262:#0D111C;--color-263:#FFF2F7;--color-264:#FFD9E4;--color-265:#FBA4C0;--color-266:#FF6FA3;--color-267:#FB118E;--color-268:#C41A69;--color-269:#8C0F49;--color-270:#55072A;--color-271:#39061B;--color-272:#2B000B;--color-273:#F51A70;--color-274:#FEF0EE;--color-275:#FED5CF;--color-276:#FEA79B;--color-277:#FD766B;--color-278:#FA2B39;--color-279:#C4292F;--color-280:#891E20;--color-281:#530F0F;--color-282:#380A03;--color-283:#240800;--color-284:#F14544;--color-285:#FEF8C4;--color-286:#F0E49A;--color-287:#DBBC19;--color-288:#BB9F13;--color-289:#A08116;--color-290:#866311;--color-291:#5D4204;--color-292:#3E2B04;--color-293:#231902;--color-294:#180F02;--color-295:#FAF40A;--color-296:#FFF5E8;--color-297:#F8DEB6;--color-298:#EEB317;--color-299:#DB900B;--color-300:#B17900;--color-301:#905C10;--color-302:#643F07;--color-303:#3F2208;--color-304:#29160F;--color-305:#161007;--color-306:#FEB239;--color-307:#EDFDF0;--color-308:#BFEECA;--color-309:#76D191;--color-310:#40B66B;--color-311:#209853;--color-312:#0B783E;--color-313:#0C522A;--color-314:#053117;--color-315:#091F10;--color-316:#09130B;--color-317:#5CFE9D;--color-318:#F3F5FE;--color-319:#DEE1FF;--color-320:#ADBCFF;--color-321:#869EFF;--color-322:#4C82FB;--color-323:#1267D6;--color-324:#1D4294;--color-325:#09265E;--color-326:#0B193F;--color-327:#040E34;--color-328:#587BFF;--color-329:#F2FEDB;--color-330:#D3EBA3;--color-331:#9BCD46;--color-332:#7BB10C;--color-333:#649205;--color-334:#527318;--color-335:#344F00;--color-336:#233401;--color-337:#171D00;--color-338:#0E1300;--color-339:#B1F13C;--color-340:#FEEDE5;--color-341:#FCD9C8;--color-342:#FBAA7F;--color-343:#F67E3E;--color-344:#DC5B14;--color-345:#AF460A;--color-346:#76330F;--color-347:#4D220B;--color-348:#2A1505;--color-349:#1C0E03;--color-350:#FF6F1E;--color-351:#FFF1FE;--color-352:#FAD8F8;--color-353:#F5A1F5;--color-354:#F06DF3;--color-355:#DC39E3;--color-356:#AF2EB4;--color-357:#7A1C7D;--color-358:#550D56;--color-359:#330733;--color-360:#250225;--color-361:#FC72FF;--color-362:#F1EFFE;--color-363:#E2DEFD;--color-364:#BDB8FA;--color-365:#9D99F5;--color-366:#7A7BEB;--color-367:#515EDC;--color-368:#343F9E;--color-369:#232969;--color-370:#121643;--color-371:#0E0D30;--color-372:#5065FD;--color-373:#D6F5FE;--color-374:#B0EDFE;--color-375:#63CDE8;--color-376:#2FB0CC;--color-377:#2092AB;--color-378:#117489;--color-379:#014F5F;--color-380:#003540;--color-381:#011E26;--color-382:#011418;--color-383:#36DBFF;--color-384:#F1FCEF;--color-385:#DAE6D8;--color-386:#B8C3B7;--color-387:#9AA498;--color-388:#7E887D;--color-389:#646B62;--color-390:#434942;--color-391:#2C302C;--color-392:#181B18;--color-393:#0F120E;--color-394:#7E887D;--color-395:#393939;--color-396:#e6e6e6;--color-397:#FA2B39;--color-398:#a26af3;--color-399:#28A0F0;--color-400:#2151F5;--color-401:#F0B90B;--color-402:#FB36D0;--color-403:#9f7750;--color-404:#C3B29E;--color-405:#1D1D20;--color-406:#86AE80;--color-407:#3E4A3C;--color-408:#081619;--color-409:#343434;--radius-1:0px;--radius-2:3px;--radius-3:5px;--radius-4:7px;--radius-5:9px;--radius-6:10px;--radius-7:16px;--radius-8:19px;--radius-9:22px;--radius-10:26px;--radius-11:34px;--radius-12:42px;--radius-13:50px;--radius-14:16px;--zIndex-1:0;--zIndex-2:100;--zIndex-3:200;--zIndex-4:300;--zIndex-5:400;--zIndex-6:500;--space-1:0px;--space-6:2px;--space-8:7px;--space-10:13px;--space-12:18px;--space-15:24px;--space-16:32px;--space-17:39px;--space-18:46px;--space-19:53px;--space-20:60px;--space-21:74px;--space-22:88px;--space-23:102px;--space-24:116px;--space-25:130px;--space-26:144px;--space-27:144px;--space-28:158px;--space-29:172px;--space-30:186px;--space-31:249px;--space-32:284px;--space-2:0.5px;--space-3:1px;--space-4:1.5px;--space-5:5px;--space-7:4px;--space-9:10px;--space-11:16px;--space-13:18px;--space-14:21px;--space-33:-0.5px;--space-34:-1px;--space-35:-1.5px;--space-36:-5px;--space-37:-2px;--space-38:-4px;--space-39:-7px;--space-40:-10px;--space-41:-13px;--space-42:-16px;--space-43:-18px;--space-44:-18px;--space-45:-21px;--space-46:-24px;--space-47:-32px;--space-48:-39px;--space-49:-46px;--space-50:-53px;--space-51:-60px;--space-52:-74px;--space-53:-88px;--space-54:-102px;--space-55:-116px;--space-56:-130px;--space-57:-144px;--space-58:-144px;--space-59:-158px;--space-60:-172px;--space-61:-186px;--space-62:-249px;--space-63:-284px;--size-1:0px;--size-6:20px;--size-8:28px;--size-10:36px;--size-12:44px;--size-15:52px;--size-16:64px;--size-17:74px;--size-18:84px;--size-19:94px;--size-20:104px;--size-21:124px;--size-22:144px;--size-23:164px;--size-24:184px;--size-25:204px;--size-26:224px;--size-27:224px;--size-28:244px;--size-29:264px;--size-30:284px;--size-31:374px;--size-32:424px;--size-2:2px;--size-3:4px;--size-4:8px;--size-5:16px;--size-7:24px;--size-9:32px;--size-11:40px;--size-13:44px;--size-14:48px} +:root .font_heading, :root .t_lang-heading-default .font_heading {--f-fa:DM Sans, -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif ;--f-si-1:11px;--f-si-2:12px;--f-si-3:13px;--f-si-4:14px;--f-si-5:16px;--f-si-6:18px;--f-si-7:20px;--f-si-8:23px;--f-si-9:30px;--f-si-10:40px;--f-si-11:55px;--f-si-12:62px;--f-si-13:72px;--f-si-14:92px;--f-si-15:114px;--f-si-16:134px;--f-si-17:14px;--f-li-1:21px;--f-li-2:22px;--f-li-3:23px;--f-li-4:24px;--f-li-5:26px;--f-li-6:28px;--f-li-7:30px;--f-li-8:33px;--f-li-9:40px;--f-li-10:50px;--f-li-11:56px;--f-li-12:65px;--f-li-13:72px;--f-li-14:82px;--f-li-15:102px;--f-li-16:124px;--f-li-17:24px} +:root .font_body, :root .t_lang-body-default .font_body {--f-fa:DM Sans, -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;--f-si-1:11px;--f-si-2:12px;--f-si-3:13px;--f-si-4:14px;--f-si-5:16px;--f-si-6:18px;--f-si-7:20px;--f-si-8:23px;--f-si-9:30px;--f-si-10:40px;--f-si-11:46px;--f-si-12:55px;--f-si-13:62px;--f-si-14:72px;--f-si-15:92px;--f-si-16:114px;--f-si-17:14px;--f-li-1:21px;--f-li-2:22px;--f-li-3:23px;--f-li-4:24px;--f-li-5:26px;--f-li-6:28px;--f-li-7:30px;--f-li-8:33px;--f-li-9:40px;--f-li-10:50px;--f-li-11:56px;--f-li-12:65px;--f-li-13:72px;--f-li-14:82px;--f-li-15:102px;--f-li-16:124px;--f-li-17:24px} +:root .font_mono, :root .t_lang-mono-default .font_mono {--f-fa:DM Mono, -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, monospace;--f-si-1:11px;--f-si-2:12px;--f-si-3:13px;--f-si-4:14px;--f-si-5:16px;--f-si-6:18px;--f-si-7:20px;--f-si-8:23px;--f-si-9:30px;--f-si-10:40px;--f-si-11:46px;--f-si-12:55px;--f-si-13:62px;--f-si-14:72px;--f-si-15:92px;--f-si-16:114px;--f-si-17:14px;--f-li-1:21px;--f-li-2:22px;--f-li-3:23px;--f-li-4:24px;--f-li-5:26px;--f-li-6:28px;--f-li-7:30px;--f-li-8:33px;--f-li-9:40px;--f-li-10:50px;--f-li-11:56px;--f-li-12:65px;--f-li-13:72px;--f-li-14:82px;--f-li-15:102px;--f-li-16:124px;--f-li-17:24px;--f-we-1:300;--f-we-2:300;--f-we-3:300;--f-we-4:400;--f-we-5:500;--f-we-6:500;--f-we-7:500;--f-we-8:500;--f-we-9:500;--f-we-10:500;--f-we-11:500;--f-we-12:500;--f-we-13:500;--f-we-14:500;--f-we-15:500;--f-we-16:500;--f-we-17:500} :root.t_dark, :root.t_dark .t_light .t_dark, :root.t_light .t_dark, :root.t_light .t_dark .t_light .t_dark {--accentBackground:var(--color-231);--accentColor:hsla(191, 32%, 10%, 1);--background0:hsla(191, 33%, 10%, 0.25);--background025:hsla(191, 33%, 10%, 0.5);--background05:hsla(191, 33%, 10%, 0.75);--background075:hsla(191, 32%, 10%, 1);--color0:hsla(112, 22%, 100%, 1);--color025:hsla(0, 0%, 100%, 0.75);--color05:hsla(0, 0%, 100%, 0.5);--color075:hsla(0, 0%, 100%, 0.25);--background:hsla(191, 32%, 10%, 1);--backgroundHover:hsla(191, 32%, 15%, 1);--backgroundPress:hsla(191, 32%, 19%, 1);--backgroundFocus:hsla(191, 32%, 24%, 1);--color:hsla(112, 22%, 59%, 1);--colorHover:hsla(191, 32%, 50%, 1);--colorPress:hsla(112, 22%, 59%, 1);--colorFocus:hsla(191, 32%, 50%, 1);--placeholderColor:hsla(191, 32%, 50%, 1);--borderColor:hsla(191, 32%, 24%, 1);--borderColorHover:hsla(191, 32%, 28%, 1);--borderColorFocus:hsla(191, 32%, 32%, 1);--borderColorPress:hsla(191, 32%, 28%, 1);--color1:hsla(191, 32%, 10%, 1);--color2:hsla(191, 32%, 15%, 1);--color3:hsla(191, 32%, 19%, 1);--color4:hsla(191, 32%, 24%, 1);--color5:hsla(191, 32%, 28%, 1);--color6:hsla(191, 32%, 32%, 1);--color7:hsla(191, 32%, 37%, 1);--color8:hsla(191, 32%, 41%, 1);--color9:hsla(191, 32%, 46%, 1);--color10:hsla(191, 32%, 50%, 1);--color11:hsla(112, 22%, 59%, 1);--color12:hsla(112, 22%, 100%, 1);} @media(prefers-color-scheme:dark){ body{background:var(--background);color:var(--color)} diff --git a/apps/next/public/vercel.svg b/apps/next/public/vercel.svg deleted file mode 100644 index fbf0e25a6..000000000 --- a/apps/next/public/vercel.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - \ No newline at end of file diff --git a/apps/next/styles/globals.css b/apps/next/styles/globals.css new file mode 100644 index 000000000..4146b4c74 --- /dev/null +++ b/apps/next/styles/globals.css @@ -0,0 +1,3 @@ +[data-rk] { + min-height: 100%; +} \ No newline at end of file diff --git a/apps/next/utils/getRemoteAssets.ts b/apps/next/utils/getRemoteAssets.ts new file mode 100644 index 000000000..b95c25ea7 --- /dev/null +++ b/apps/next/utils/getRemoteAssets.ts @@ -0,0 +1,7 @@ +import { getPlaiceholderImage } from 'app/utils/getPlaiceholderImage' + +export async function getRemoteAssets(paths: string[] = []) { + const remoteImageUrl = 'https://github.com/0xsend/assets/blob/main/' + const imagePromises = paths.map((path) => getPlaiceholderImage(remoteImageUrl + path)) + return await Promise.all(imagePromises) +} diff --git a/apps/next/utils/guestOnly.ts b/apps/next/utils/guestOnly.ts index 26272b206..ffd433f4d 100644 --- a/apps/next/utils/guestOnly.ts +++ b/apps/next/utils/guestOnly.ts @@ -1,7 +1,7 @@ -import { ParsedUrlQuery } from 'querystring' -import { Database } from '@my/supabase/database.types' +import type { ParsedUrlQuery } from 'node:querystring' +import type { Database } from '@my/supabase/database.types' import { createPagesServerClient } from '@supabase/auth-helpers-nextjs' -import { GetServerSideProps, PreviewData } from 'next' +import type { GetServerSideProps, PreviewData } from 'next' /** * user protected getServerSideProps - pass your own function as the only arg diff --git a/apps/next/utils/userOnboarded.ts b/apps/next/utils/userOnboarded.ts index 97472ffef..3ac4c3cf4 100644 --- a/apps/next/utils/userOnboarded.ts +++ b/apps/next/utils/userOnboarded.ts @@ -1,6 +1,6 @@ -import { ParsedUrlQuery } from 'querystring' -import { Database } from '@my/supabase/database.types' -import { GetServerSidePropsContext, PreviewData, Redirect } from 'next' +import type { ParsedUrlQuery } from 'node:querystring' +import type { Database } from '@my/supabase/database.types' +import type { GetServerSidePropsContext, PreviewData, Redirect } from 'next' import type { SupabaseClient } from '@supabase/supabase-js' import debug from 'debug' @@ -34,7 +34,7 @@ export async function userOnboarded< log('no send accounts') return { redirect: { - destination: '/onboarding', // @todo add query param to redirect back to page after onboarding + destination: '/auth/onboarding', // @todo add query param to redirect back to page after onboarding permanent: false, }, } diff --git a/apps/next/utils/userProtected.ts b/apps/next/utils/userProtected.ts index e05dfc77a..cf8734ae0 100644 --- a/apps/next/utils/userProtected.ts +++ b/apps/next/utils/userProtected.ts @@ -1,7 +1,7 @@ -import { ParsedUrlQuery } from 'querystring' -import { Database } from '@my/supabase/database.types' +import type { ParsedUrlQuery } from 'node:querystring' +import type { Database } from '@my/supabase/database.types' import { createPagesServerClient } from '@supabase/auth-helpers-nextjs' -import { GetServerSideProps, PreviewData } from 'next' +import type { GetServerSideProps, PreviewData } from 'next' import debug from 'debug' import { userOnboarded } from './userOnboarded' @@ -37,14 +37,15 @@ export function userProtectedGetSSP< log('no session') return { redirect: { - destination: '/sign-in', // @todo add query param to redirect back to page after sign in + destination: '/auth/sign-in', // @todo add query param to redirect back to page after sign in permanent: false, }, } } - const needsOnboarding = await userOnboarded(supabase, ctx) - if (needsOnboarding) return needsOnboarding + // @todo re-introduce once onboarding is ready + // const needsOnboarding = await userOnboarded(supabase, ctx) + // if (needsOnboarding) return needsOnboarding const getSSRResult = getServerSideProps ? await getServerSideProps(ctx) : { props: {} as Props } if ('props' in getSSRResult) { diff --git a/biome.json b/biome.json index c9ef106be..87864b473 100644 --- a/biome.json +++ b/biome.json @@ -19,7 +19,10 @@ "**/theme-generated.ts", "./apps/expo/ios/**", "./apps/expo/android/**", - "./packages/app/components/img/**" + "./packages/app/components/img/**", + "packages/shovel/etc/config.json", + "./supabase/.temp/**", + "./packages/contracts/var/*.json" ] }, "organizeImports": { diff --git a/docs/design/send-app-onboarding.md b/docs/design/send-app-onboarding.md index 7a77725e8..279f983f7 100644 --- a/docs/design/send-app-onboarding.md +++ b/docs/design/send-app-onboarding.md @@ -5,7 +5,7 @@ Send is a mobile app that allows users to send and receive crypto payments. It i - buy, sell, send, and spend Ethereum - self-custodial onchain payments (tap to pay) - custody of $ETH, $USDT, $USDC, and $SEND only -- p2p transfers by way of Send Tags not addresses +- p2p transfers by way of Sendtags not addresses ## Getting Started diff --git a/docs/design/send-app-screens.md b/docs/design/send-app-screens.md index 352e31ca9..329c2e908 100644 --- a/docs/design/send-app-screens.md +++ b/docs/design/send-app-screens.md @@ -8,7 +8,7 @@ - Phone Number - Notifications - Account - - Send Tags + - Sendtags - Profile - Bio - PFP diff --git a/environment.d.ts b/environment.d.ts new file mode 100644 index 000000000..ea46b997a --- /dev/null +++ b/environment.d.ts @@ -0,0 +1,36 @@ +declare global { + namespace NodeJS { + interface ProcessEnv { + PORT?: string + NODE_ENV: 'development' | 'production' | 'test' + SUPABASE_JWT_SECRET: string + + EXPO_PUBLIC_URL: string + NEXT_PUBLIC_URL: string + + EXPO_PUBLIC_SUPABASE_URL: string + NEXT_PUBLIC_SUPABASE_URL: string + + EXPO_PUBLIC_SUPABASE_ANON_KEY: string + NEXT_PUBLIC_SUPABASE_ANON_KEY: string + NEXT_PUBLIC_SUPABASE_PROJECT_ID: string + NEXT_PUBLIC_SUPABASE_GRAPHQL_URL: string + NEXT_PUBLIC_MAINNET_RPC_URL: string + NEXT_PUBLIC_BASE_RPC_URL: string + NEXT_PUBLIC_BUNDLER_RPC_URL: string + SUPABASE_DB_URL: string + SUPABASE_SERVICE_ROLE: string + NEXT_PUBLIC_MAINNET_CHAIN_ID: string + NEXT_PUBLIC_BASE_CHAIN_ID: string + SNAPLET_HASH_KEY: string + } + } + /** + * This variable is set to true when react-native is running in Dev mode + * @example + * if (__DEV__) console.log('Running in dev mode') + */ + const __DEV__: boolean +} + +export type {} diff --git a/globals.d.ts b/globals.d.ts new file mode 100644 index 000000000..25089b8f1 --- /dev/null +++ b/globals.d.ts @@ -0,0 +1,11 @@ +export type {} + +declare global { + /** + * This variable is set to true when react-native is running in Dev mode. + * Ported here so we can share some react native code. + * @example + * if (__DEV__) console.log('Running in dev mode') + */ + const __DEV__: boolean +} diff --git a/package.json b/package.json index 4ae3a10ee..6e0ddaa53 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "web:prod": "yarn workspace next-app build", "web:prod:serve": "yarn workspace next-app serve", "fix": "manypkg fix", - "postinstall": "test -n \"$SKIP_YARN_POST_INSTALL\" || (yarn check-deps && yarn build)", + "postinstall": "test -n \"$SKIP_YARN_POST_INSTALL\" || (yarn check-deps && turbo build --filter='!next-app')", "build": "yarn workspaces foreach --all --exclude next-app run build", "biome:check": "biome check .", "biome:check:fix": "biome check . --apply", @@ -31,14 +31,17 @@ "playwright": "yarn workspace @my/playwright", "distributor": "yarn workspace distributor", "snaplet:seed": "DRY=0 bunx tsx ./packages/snaplet/seed.ts", - "snaplet:snapshot:restore": "bunx snaplet snapshot restore --no-reset --latest" + "snaplet:snapshot:restore": "bunx snaplet snapshot restore --no-reset --latest", + "clean": "yarn workspaces foreach --all -pi run clean" }, "resolutions": { "react": "^18.2.0", "react-dom": "^18.2.0", "react-refresh": "^0.14.0", - "react-native-svg": "13.9.0", - "react-native-web": "~0.19.6" + "react-native-svg": "14.1.0", + "react-native-web": "~0.19.10", + "@babel/core": "^7.20.2", + "babel-loader": "^8.3.0" }, "dependencies": { "@babel/runtime": "^7.18.9", @@ -48,14 +51,14 @@ "eslint": "^8.46.0", "node-gyp": "^9.3.1", "pierre": "^2.0.0-alpha.8", - "react-native-ios-modal": "^0.1.8", "turbo": "^1.10.3", - "typescript": "^5.1.3" + "typescript": "^5.3.3" }, "packageManager": "yarn@4.0.2", "devDependencies": { - "@biomejs/biome": "1.5.3", + "@biomejs/biome": "^1.6.3", "jest": "^29.7.0", + "jest-expo": "^50.0.0", "lefthook": "^1.5.5" } } diff --git a/packages/api/package.json b/packages/api/package.json index b6de8f564..e9479527b 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -5,6 +5,7 @@ "private": true, "dependencies": { "@my/supabase": "workspace:*", + "@my/wagmi": "workspace:*", "@tanstack/react-query": "^5.18.1", "@trpc/client": "11.0.0-next-beta.264", "@trpc/next": "11.0.0-next-beta.264", @@ -12,6 +13,7 @@ "@trpc/server": "11.0.0-next-beta.264", "app": "workspace:*", "superjson": "^1.13.1", + "viem": "^2.8.10", "zod": "^3.22.4" } } diff --git a/packages/api/src/routers/_app.ts b/packages/api/src/routers/_app.ts index 2fb956182..1b7cfd83d 100644 --- a/packages/api/src/routers/_app.ts +++ b/packages/api/src/routers/_app.ts @@ -1,15 +1,17 @@ -import { inferRouterInputs, inferRouterOutputs } from '@trpc/server' +import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server' import { createTRPCRouter } from '../trpc' import { authRouter } from './auth' import { chainAddressRouter } from './chainAddress' import { distributionRouter } from './distribution' import { tagRouter } from './tag' +import { secretShopRouter } from './secretShop' export const appRouter = createTRPCRouter({ chainAddress: chainAddressRouter, tag: tagRouter, auth: authRouter, distribution: distributionRouter, + secretShop: secretShopRouter, }) export type AppRouter = typeof appRouter diff --git a/packages/api/src/routers/chainAddress.ts b/packages/api/src/routers/chainAddress.ts index f98fb5c95..e05a6d25d 100644 --- a/packages/api/src/routers/chainAddress.ts +++ b/packages/api/src/routers/chainAddress.ts @@ -1,7 +1,7 @@ import { TRPCError } from '@trpc/server' -import { verifyAddressMsg } from 'app/features/checkout/screen' +import { verifyAddressMsg } from 'app/features/account/sendtag/checkout/checkout-utils' import { supabaseAdmin } from 'app/utils/supabase/admin' -import { verifyMessage } from 'viem' +import { verifyMessage, isAddress, getAddress } from 'viem' import { z } from 'zod' import { createTRPCRouter, protectedProcedure } from '../trpc' @@ -13,9 +13,16 @@ export const chainAddressRouter = createTRPCRouter({ address: z.string().regex(/^0x[0-9a-f]{40}$/i), }) ) - .mutation(async ({ ctx: { session }, input: { address, signature } }) => { + .mutation(async ({ ctx: { session }, input: { address: addressInput, signature } }) => { + if (!isAddress(addressInput, { strict: false })) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Invalid address.', + }) + } + const address = getAddress(addressInput) const verified = await verifyMessage({ - address: address as `0x${string}`, + address: address, message: verifyAddressMsg(address), signature: signature as `0x${string}`, }).catch((e) => { @@ -23,7 +30,10 @@ export const chainAddressRouter = createTRPCRouter({ }) if (!verified) { - throw new TRPCError({ code: 'BAD_REQUEST', message: 'Signature verification failed.' }) + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Signature verification failed.', + }) } const { data: results, error } = await supabaseAdmin.from('chain_addresses').insert({ @@ -33,8 +43,14 @@ export const chainAddressRouter = createTRPCRouter({ if (error) { if (error.message.includes('duplicate key value violates unique constraint')) - throw new TRPCError({ code: 'BAD_REQUEST', message: 'Address already exists.' }) - throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: error.message }) + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Address already exists.', + }) + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: error.message, + }) } return results diff --git a/packages/api/src/routers/secretShop.ts b/packages/api/src/routers/secretShop.ts new file mode 100644 index 000000000..4b153e52a --- /dev/null +++ b/packages/api/src/routers/secretShop.ts @@ -0,0 +1,160 @@ +import { TRPCError } from '@trpc/server' +import { z } from 'zod' +import { createTRPCRouter, protectedProcedure } from '../trpc' +import { + isAddress, + getAddress, + createWalletClient, + http, + publicActions, + getContract, + parseEther, +} from 'viem' +import type { PrivateKeyAccount } from 'viem/accounts' +import { baseMainnet } from '@my/wagmi/chains' +import { baseMainnetClient } from 'app/utils/viem' +import { privateKeyToAccount } from 'viem/accounts' +import { assert } from 'app/utils/assert' +import { erc20Abi, sendTokenAddress, usdcAddress } from '@my/wagmi' +import { waitForTransactionReceipt } from 'viem/actions' + +export const secretShopRouter = createTRPCRouter({ + fund: protectedProcedure + .input( + z.object({ + address: z.string().regex(/^0x[0-9a-f]{40}$/i), + }) + ) + .mutation(async ({ input: { address: addressInput } }) => { + if (!isAddress(addressInput, { strict: false })) { + throw new TRPCError({ code: 'BAD_REQUEST', message: 'Invalid address.' }) + } + const address = getAddress(addressInput) + + assert(!!process.env.SECRET_SHOP_PRIVATE_KEY, 'SECRET_SHOP_PRIVATE_KEY is required') + + let secretShopAccount: PrivateKeyAccount + try { + secretShopAccount = privateKeyToAccount( + process.env.SECRET_SHOP_PRIVATE_KEY as `0x${string}` + ) + console.log('secretShopAccount', secretShopAccount.address) + } catch (e) { + if (e instanceof Error) { + throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: e.message }) + } + throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Unknown error' }) + } + + const secretShopClient = createWalletClient({ + account: secretShopAccount, + chain: baseMainnet, + transport: http(baseMainnetClient.transport.url), + }).extend(publicActions) + + const sendToken = getContract({ + abi: erc20Abi, + address: sendTokenAddress[baseMainnet.id], + client: secretShopClient, + }) + const usdcToken = getContract({ + abi: erc20Abi, + address: usdcAddress[baseMainnet.id], + client: secretShopClient, + }) + + const [ethBal, sendBal, usdcBal, ssEthBal, ssUsdcBal, ssSendBal] = await Promise.all([ + secretShopClient.getBalance({ address }), + sendToken.read.balanceOf([address]), + usdcToken.read.balanceOf([address]), + secretShopClient.getBalance({ address: secretShopAccount.address }), + usdcToken.read.balanceOf([secretShopAccount.address]), + sendToken.read.balanceOf([secretShopAccount.address]), + ] as const) + + // fund account where balances are low + + // eth + let ethTxHash: null | string = null + const eth = parseEther('0.1') + if (ssEthBal < eth + BigInt(1e6)) { + throw new TRPCError({ code: 'BAD_REQUEST', message: 'Insufficient ETH in secret shop' }) + } + if (ethBal < eth) { + await secretShopClient + .sendTransaction({ + to: address, + value: eth - ethBal, // only transfer the difference + chain: baseMainnet, + }) + .then((hash) => { + return waitForTransactionReceipt(secretShopClient, { + hash, + }) + }) + .then((receipt) => { + ethTxHash = receipt.transactionHash + }) + .catch((e) => { + ethTxHash = e.message + }) + } + // usdc + let usdcTxHash: null | string = null + const usdc = BigInt(5e6) // $5 worth of USDC + if (usdcBal < usdc) { + if (ssUsdcBal < usdc) { + usdcTxHash = 'Error: Insufficient USDC in secret shop' + } else { + await usdcToken.write + // only transfer the difference + .transfer([address, usdc - usdcBal], { + chain: baseMainnet, + }) + .then((hash) => { + return waitForTransactionReceipt(secretShopClient, { + hash, + }) + }) + .then((receipt) => { + usdcTxHash = receipt.transactionHash + }) + .catch((e) => { + usdcTxHash = e.message + }) + } + } + + // send + let sendTxHash: null | string = null + const send = BigInt(1e5) // 100K SEND + if (sendBal < send) { + if (ssSendBal < send) { + sendTxHash = 'Error: Insufficient SEND in secret shop' + } else { + await sendToken.write + // only transfer the difference + .transfer([address, send - sendBal], { + chain: baseMainnet, + }) + .then((hash) => { + return waitForTransactionReceipt(secretShopClient, { + hash, + }) + }) + .then((receipt) => { + sendTxHash = receipt.transactionHash + }) + .catch((e) => { + sendTxHash = e.message + }) + } + } + + return { + ethTxHash, + usdcTxHash, + sendTxHash, + } + }), +}) diff --git a/packages/api/src/routers/tag.ts b/packages/api/src/routers/tag.ts index 5063fadaf..55972cad4 100644 --- a/packages/api/src/routers/tag.ts +++ b/packages/api/src/routers/tag.ts @@ -1,7 +1,10 @@ import { TRPCError } from '@trpc/server' -import { getPriceInWei, getSenderSafeReceivedEvents } from 'app/features/checkout/screen' +import { + getPriceInWei, + getSenderSafeReceivedEvents, +} from 'app/features/account/sendtag/checkout/checkout-utils' import { supabaseAdmin } from 'app/utils/supabase/admin' -import { mainnetClient } from '@my/wagmi' +import { baseMainnetClient } from '@my/wagmi' import debug from 'debug' import { isAddressEqual } from 'viem' import { z } from 'zod' @@ -30,15 +33,17 @@ export const tagRouter = createTRPCRouter({ message: 'No tags to confirm.', }) } - throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: tagsError.message }) + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: tagsError.message, + }) } const pendingTags = tags.filter((t) => t.status === 'pending') - const confirmedTags = tags.filter((t) => t.status === 'confirmed') - const ethAmount = getPriceInWei(pendingTags, confirmedTags) + const ethAmount = getPriceInWei(pendingTags) const isFree = ethAmount === BigInt(0) - // transaction validation for rare Send Tags + // transaction validation for rare Sendtags if (!isFree) { if (!txHash) { log('transaction hash required') @@ -73,15 +78,18 @@ export const tagRouter = createTRPCRouter({ // get transaction receipt const [receipt, confirmations] = await Promise.all([ - mainnetClient.getTransactionReceipt({ + baseMainnetClient.getTransactionReceipt({ hash: txHash as `0x${string}`, }), - mainnetClient.getTransactionConfirmations({ + baseMainnetClient.getTransactionConfirmations({ hash: txHash as `0x${string}`, }), ]).catch((error) => { log('transaction error', error) - throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: error.message }) + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: error.message, + }) }) // check if transaction is confirmed by at least 2 blocks @@ -104,11 +112,14 @@ export const tagRouter = createTRPCRouter({ // validate transaction is payment for tags const eventLogs = await getSenderSafeReceivedEvents({ - publicClient: mainnetClient, + publicClient: baseMainnetClient, sender: receipt.from, }).catch((error) => { log('get events error', error) - throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: error.message }) + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: error.message, + }) }) const eventLog = eventLogs.find((e) => e.transactionHash === txHash) @@ -193,7 +204,10 @@ export const tagRouter = createTRPCRouter({ if (confirmTagsErr) { log('confirm tags error', confirmTagsErr) - throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: confirmTagsErr.message }) + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: confirmTagsErr.message, + }) } log( diff --git a/packages/api/src/trpc.ts b/packages/api/src/trpc.ts index 2c43807df..05024f477 100644 --- a/packages/api/src/trpc.ts +++ b/packages/api/src/trpc.ts @@ -1,7 +1,7 @@ -import { Database } from '@my/supabase/database.types' +import type { Database } from '@my/supabase/database.types' import { createPagesServerClient } from '@supabase/auth-helpers-nextjs' import { TRPCError, initTRPC } from '@trpc/server' -import { type CreateNextContextOptions } from '@trpc/server/adapters/next' +import type { CreateNextContextOptions } from '@trpc/server/adapters/next' import superJson from 'superjson' export const createTRPCContext = async (opts: CreateNextContextOptions) => { diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json new file mode 100644 index 000000000..3da2fe828 --- /dev/null +++ b/packages/api/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base", + "include": ["src", "../app", "../wagmi/src", "../ui/src"], + "compilerOptions": { + "noEmit": true, + "composite": true, + "jsx": "react-jsx" + }, + "references": [] +} diff --git a/packages/app/__mocks__/app/provider.tsx b/packages/app/__mocks__/app/provider.tsx deleted file mode 100644 index 69f7ab0d5..000000000 --- a/packages/app/__mocks__/app/provider.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { TamaguiProvider, config } from '@my/ui' - -const mockProvider = { - Provider: ({ children }: { children: React.ReactNode }) => ( - - {children} - - ), -} - -export const Provider = mockProvider.Provider -export default mockProvider diff --git a/packages/app/__mocks__/app/provider/index.tsx b/packages/app/__mocks__/app/provider/index.tsx new file mode 100644 index 000000000..3fcceff0a --- /dev/null +++ b/packages/app/__mocks__/app/provider/index.tsx @@ -0,0 +1,15 @@ +import { TamaguiProvider, config } from '@my/ui' + +const mockProvider = { + Provider: ({ children }: { children: React.ReactNode }) => { + // console.log('mockProvider') + return ( + + {children} + + ) + }, +} + +export const Provider = mockProvider.Provider +export default mockProvider diff --git a/packages/app/__mocks__/app/utils/useUser.ts b/packages/app/__mocks__/app/utils/useUser.ts new file mode 100644 index 000000000..16dc55d4d --- /dev/null +++ b/packages/app/__mocks__/app/utils/useUser.ts @@ -0,0 +1,20 @@ +const mock = { + useUser: jest.fn().mockReturnValue({ + profile: { + name: 'No Name', + avatar_url: 'https://example.com', + }, + user: { + id: '123', + }, + tags: [ + { + name: 'tag1', + }, + ], + }), +} + +export const useUser = mock.useUser + +export default mock diff --git a/packages/app/__mocks__/wagmi.ts b/packages/app/__mocks__/wagmi.ts index 941a323e3..b1a8318b3 100644 --- a/packages/app/__mocks__/wagmi.ts +++ b/packages/app/__mocks__/wagmi.ts @@ -1,6 +1,8 @@ const mockWagmi = { useChainId: jest.fn().mockReturnValue(845337), - createConfig: jest.fn().mockReturnValue({}), + createConfig: jest.fn().mockReturnValue({ + chains: [845337], + }), useBalance: jest.fn().mockReturnValue({ data: { decimals: 6, @@ -22,6 +24,50 @@ const mockWagmi = { isSuccess: true, error: null, }), + useAccount: jest.fn().mockReturnValue({ + isConnected: true, + address: '0x123', + chain: 845337, + }), + useConnect: jest.fn().mockReturnValue({ + connect: jest.fn(), + connectors: [], + error: null, + }), + useSwitchChain: jest.fn().mockReturnValue({ + chains: [ + { + id: 845337, + name: 'Base', + }, + ], + switchChain: jest.fn(), + error: null, + }), + useWriteContract: jest.fn().mockReturnValue({ + data: '0x123', + writeContract: jest.fn(), + isPending: false, + error: null, + }), + useWaitForTransactionReceipt: jest.fn().mockReturnValue({ + data: { + blockHash: '0x123', + blockNumber: 123, + contractAddress: '0x123', + cumulativeGasUsed: 123, + from: '0x123', + gasUsed: 123, + logs: [], + logsBloom: '0x123', + status: 1, + to: '0x123', + transactionHash: '0x123', + transactionIndex: 123, + }, + isSuccess: true, + error: null, + }), } export const useChainId = mockWagmi.useChainId @@ -29,5 +75,9 @@ export const createConfig = mockWagmi.createConfig export const useBalance = mockWagmi.useBalance export const useBytecode = mockWagmi.useBytecode export const useTransactionCount = mockWagmi.useTransactionCount - +export const useAccount = mockWagmi.useAccount +export const useConnect = mockWagmi.useConnect +export const useSwitchChain = mockWagmi.useSwitchChain +export const useWriteContract = mockWagmi.useWriteContract +export const useWaitForTransactionReceipt = mockWagmi.useWaitForTransactionReceipt export default mockWagmi diff --git a/packages/app/components/HomeHeader.tsx b/packages/app/components/HomeHeader.tsx deleted file mode 100644 index 5025d2198..000000000 --- a/packages/app/components/HomeHeader.tsx +++ /dev/null @@ -1,209 +0,0 @@ -import { - Anchor, - Button, - H1, - Header, - Link, - Text, - View, - XStack, - YStack, - useToastController, -} from '@my/ui' -import { useNav } from 'app/routers/params' -import { useUserReferralsCount } from 'app/utils/useUserReferralsCount' -import { getReferralHref } from 'app/utils/getReferralLink' -import { useUser } from 'app/utils/useUser' -import { useAccount, useConnect, useDisconnect } from 'wagmi' -import { IconClose, IconCopy, IconGear, IconHamburger, IconStar } from 'app/components/icons' -import { usePathname } from 'app/utils/usePathname' - -// TODO: this should probably named HomeTopNav -export function HomeHeader({ children }: { children: string }) { - const [nav, setNavParam] = useNav() - const handleHomeBottomSheet = () => { - setNavParam(nav ? undefined : 'home', { webBehavior: 'replace' }) - } - - return ( -
- -

{children}

- - - - - - - -
- ) -} - -function ReferralCodeCard() { - const user = useUser() - const toast = useToastController() - const referralHref = getReferralHref(user?.profile?.referral_code ?? '') - - if (!user?.profile?.referral_code) { - return null - } - - return ( - - - - - Referral Link - - - send.it/{user?.profile?.referral_code} - - - - - - ) - } - return ( - - {address} - + ) : distributionNumberParam === distribution?.number || + (distributionNumberParam === undefined && + distribution?.number === distributions?.length) ? ( + + + + + + ) : ( + + ) + })} + + + + ) +} + +const DistributionRewardsSkeleton = () => { + return null +} diff --git a/packages/app/features/account/screen.test.tsx b/packages/app/features/account/screen.test.tsx new file mode 100644 index 000000000..c526d0d78 --- /dev/null +++ b/packages/app/features/account/screen.test.tsx @@ -0,0 +1,29 @@ +import { Wrapper } from 'app/utils/__mocks__/Wrapper' +import { AccountScreen } from './screen' +import { render, screen, act } from '@testing-library/react-native' + +jest.mock('app/utils/useUser') +jest.mock('app/utils/tags', () => ({ + useConfirmedTags: jest.fn().mockReturnValue([{ name: 'test' }]), +})) +jest.mock('app/utils/getReferralLink', () => ({ + getReferralHref: jest.fn().mockReturnValue('https://send.it/123'), +})) +jest.mock('app/routers/params', () => ({ + useNav: jest.fn().mockReturnValue([undefined, jest.fn()]), +})) +describe('AccountScreen', () => { + it('renders the account screen', async () => { + jest.useFakeTimers() + render( + + + + ) + await act(async () => { + jest.runAllTimers() + }) + + expect(screen.toJSON()).toMatchSnapshot('AccountScreen') + }) +}) diff --git a/packages/app/features/account/screen.tsx b/packages/app/features/account/screen.tsx new file mode 100644 index 000000000..7eee3157c --- /dev/null +++ b/packages/app/features/account/screen.tsx @@ -0,0 +1,239 @@ +import { + Avatar, + Container, + Link, + type LinkProps, + Paragraph, + Separator, + XStack, + YStack, + Button, + useToastController, + TooltipSimple, + useMedia, + useThemeName, + Theme, +} from '@my/ui' +import { + IconAccount, + IconCopy, + IconDollar, + IconGear, + IconPlus, + IconShare, +} from 'app/components/icons' +import { getReferralHref } from 'app/utils/getReferralLink' +import { useUser } from 'app/utils/useUser' +import * as Clipboard from 'expo-clipboard' +import * as Sharing from 'expo-sharing' +import { useNav } from 'app/routers/params' +import type React from 'react' +import { type ElementType, useEffect, useState } from 'react' +import { useThemeSetting } from '@tamagui/next-theme' +import { useConfirmedTags } from 'app/utils/tags' + +export function AccountScreen() { + const media = useMedia() + const toast = useToastController() + const { profile } = useUser() + const name = profile?.name + const send_id = profile?.send_id + const avatar_url = profile?.avatar_url + const tags = useConfirmedTags() + const sendTags = useConfirmedTags()?.reduce((prev, tag) => `${prev} @${tag.name}`, '') + const refCode = profile?.referral_code ?? '' + const referralHref = getReferralHref(refCode) + const [, setNavParam] = useNav() + const [canShare, setCanShare] = useState(false) + + useEffect(() => { + const canShare = async () => { + const canShare = await Sharing.isAvailableAsync() + setCanShare(canShare) + } + canShare() + }, []) + + const shareOrCopyOnPress = async () => { + if (canShare) { + return await Sharing.shareAsync(referralHref) + } + + await Clipboard.setStringAsync(referralHref) + .then(() => toast.show('Copied your referral link to the clipboard')) + .catch(() => + toast.show('Something went wrong', { + message: 'We were unable to copy your referral link to the clipboard', + customData: { + theme: 'error', + }, + }) + ) + } + + const facts = [ + { label: 'Send ID', value: send_id }, + { label: 'Sendtags', value: sendTags }, + { + label: 'Referral Code', + value: ( + + + + ), + }, + ] + + return ( + <> + + + + + + + + + + + + + {name ? name : '---'} + + {tags?.[0] ? ( + + @{tags[0].name} + + ) : null} + + + + + Sendtags + + + Rewards + + { + if (media.lg) { + e.preventDefault() + setNavParam('settings', { webBehavior: 'replace' }) + } + }, + } + : {})} + > + Settings + + + + + + + + Sendtags + + + Rewards + + + + + + ) +} + +const BorderedLink = ({ + Icon, + children, + ...props +}: { Icon?: ElementType; children: React.ReactNode } & LinkProps) => { + const themeName = useThemeName() + const { resolvedTheme } = useThemeSetting() + const iconColor = (resolvedTheme ?? themeName)?.startsWith('dark') ? '$color10' : '$color1' + return ( + + + {Icon && } + + {children} + + + + ) +} + +const ProfileFacts = ({ facts }: { facts: { label: string; value?: React.ReactNode }[] }) => { + return ( + <> + + + {facts.map((fact) => ( + + {fact.label} + + ))} + + + {facts.map((fact) => ( + + {fact.value ? fact.value : `No ${fact.label.toLowerCase()}`} + + ))} + + + + {facts.map((fact) => ( + + + {fact.label} + + + {fact.value ? fact.value : `No ${fact.label.toLowerCase()}`} + + + ))} + + + ) +} diff --git a/packages/app/features/checkout/CheckoutTagSchema.tsx b/packages/app/features/account/sendtag/checkout/CheckoutTagSchema.tsx similarity index 82% rename from packages/app/features/checkout/CheckoutTagSchema.tsx rename to packages/app/features/account/sendtag/checkout/CheckoutTagSchema.tsx index 5f54a2355..41e1f580d 100644 --- a/packages/app/features/checkout/CheckoutTagSchema.tsx +++ b/packages/app/features/account/sendtag/checkout/CheckoutTagSchema.tsx @@ -5,7 +5,7 @@ export const CheckoutTagSchema = z.object({ name: formFields.text .min(1) .max(20) + .trim() // English alphabet, numbers, and underscore - .regex(/^[a-zA-Z0-9_]+$/, 'Only English alphabet, numbers, and underscore') - .describe('Name // Your Send Tag name'), + .regex(/^[a-zA-Z0-9_]+$/, 'Only English alphabet, numbers, and underscore'), }) diff --git a/packages/app/features/account/sendtag/checkout/SendTagPricingDialog.tsx b/packages/app/features/account/sendtag/checkout/SendTagPricingDialog.tsx new file mode 100644 index 000000000..f1348ee4b --- /dev/null +++ b/packages/app/features/account/sendtag/checkout/SendTagPricingDialog.tsx @@ -0,0 +1,382 @@ +import type { Tables } from '@my/supabase/database.types' +import { + Adapt, + Button, + ButtonIcon, + ButtonText, + Dialog, + H6, + Paragraph, + Separator, + Sheet, + SizableText, + Tooltip, + Unspaced, + XStack, + YStack, +} from '@my/ui' +import { Info, X, XCircle } from '@tamagui/lucide-icons' +import { IconInfoGreenCircle } from 'app/components/icons' +import React, { useMemo, useState } from 'react' +import { formatEther } from 'viem' +import { getPriceInWei } from './checkout-utils' + +export function SendTagPricingDialog({ name = '' }: { name: Tables<'tags'>['name'] }) { + const price = useMemo(() => getPriceInWei([{ name }]), [name]) + const [isOpen, setIsOpen] = useState(false) + return ( + + + + + + + + + + + + + + + + + + + Sendtag Pricing + + + Sendtags are priced based on their length. The shorter the Sendtag, the more it costs. + + + + + 6+ characters + + + + {(0.002).toLocaleString()} ETH + + + + + 5 characters + + + + {(0.005).toLocaleString()} ETH + + + + + 4 characters + + + {(0.01).toLocaleString()} ETH + + + + + 1-3 characters + + + + {(0.02).toLocaleString()} ETH + + + + + + + + + + + + + + + + + + + ) +} + +export function SendTagPricingTooltip({ name = '' }: { name: Tables<'tags'>['name'] }) { + const price = useMemo(() => getPriceInWei([{ name }]), [name]) + const [isOpen, setIsOpen] = React.useState(false) + return ( + + + + + + {isOpen && ( + + )} + + + + + +
+ Send Tag Pricing +
+ + Sendtag price is based on length — the shorter it is, the higher the price. + + + + + + 6+ characters + + + + {(0.002).toLocaleString()} ETH + + + + + 5 characters + + + + {(0.005).toLocaleString()} ETH + + + + + 4 characters + + + {(0.01).toLocaleString()} ETH + + + + + 1-3 characters + + + + {(0.02).toLocaleString()} ETH + + + +
+
+
+ ) +} + +const SendTagPricingButton = ({ + isOpen, + name, + price, +}: { isOpen: boolean; name: string; price: bigint }) => { + return ( + + ) +} diff --git a/packages/app/features/account/sendtag/checkout/checkout-form.tsx b/packages/app/features/account/sendtag/checkout/checkout-form.tsx new file mode 100644 index 000000000..d008acdf7 --- /dev/null +++ b/packages/app/features/account/sendtag/checkout/checkout-form.tsx @@ -0,0 +1,385 @@ +import { + AnimatePresence, + Button, + ButtonText, + Label, + Paragraph, + Stack, + SubmitButton, + Theme, + XStack, + YStack, + useToastController, + useMedia, +} from '@my/ui' + +import { X } from '@tamagui/lucide-icons' +import { SchemaForm } from 'app/utils/SchemaForm' +import { useSupabase } from 'app/utils/supabase/useSupabase' +import { useConfirmedTags, usePendingTags } from 'app/utils/tags' +import { useChainAddresses } from 'app/utils/useChainAddresses' +import { useTimeRemaining } from 'app/utils/useTimeRemaining' +import { useUser } from 'app/utils/useUser' +import React, { useMemo } from 'react' +import { FormProvider, useForm } from 'react-hook-form' +import { formatEther } from 'viem' +import type { z } from 'zod' +import { CheckoutTagSchema } from './CheckoutTagSchema' +import { SendTagPricingDialog, SendTagPricingTooltip } from './SendTagPricingDialog' +import { getPriceInWei, maxNumSendTags, tagLengthToWei } from './checkout-utils' +import { IconPlus } from 'app/components/icons' +import { OpenConnectModalWrapper } from 'app/utils/OpenConnectModalWrapper' +import { ConfirmButton } from './components/checkout-confirm-button' +import { useRouter } from 'solito/router' + +export const CheckoutForm = () => { + const user = useUser() + const pendingTags = usePendingTags() + const confirmedTags = useConfirmedTags() + const hasPendingTags = pendingTags && pendingTags.length > 0 + const form = useForm>() + const supabase = useSupabase() + const toast = useToastController() + const has5Tags = user?.tags?.length === 5 + const [needsVerification, setNeedsVerification] = React.useState(false) + const media = useMedia() + const router = useRouter() + + const { data: addresses } = useChainAddresses() + + async function createSendTag({ name }: z.infer) { + setNeedsVerification(false) // reset verification state + + if (!user.user) return console.error('No user') + const { error } = await supabase.from('tags').insert({ name }) + + if (error) { + console.error("Couldn't create Sendtag", error) + switch (error.code) { + case '23505': + form.setError('name', { type: 'custom', message: 'This Sendtag is already taken' }) + break + case 'P0001': + if (error.message?.includes(`You don't got the riz for the tag:`)) { + setNeedsVerification(!!addresses && addresses.length === 0) + } + form.setError('name', { + type: 'custom', + message: error.message ?? 'Something went wrong', + }) + break + default: + form.setError('name', { + type: 'custom', + message: error.message ?? 'Something went wrong', + }) + break + } + } else { + // form state is successfully submitted, show the purchase confirmation screen + form.reset() + user?.updateProfile() + } + } + + function onConfirmed() { + user?.updateProfile() + router.replace('/account/sendtag') + } + + return ( + + ( + + {!has5Tags && ( + + submit()} + $gtSm={{ miw: 200 }} + br={12} + icon={} + > + + ADD TAG + + + {media.gtMd ? ( + + ) : ( + + )} + + )} + {hasPendingTags ? ( + + + + Your Sendtags are not confirmed until payment is received and your wallet is + verified + + + + Sendtag + + + Expires In + + + Price + + + {pendingTags + ?.sort( + (a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime() + ) + .map((tag) => ( + + + {tag.name} + + + + + + + + + + + + ))} + + ) : null} + + )} + > + {(fields) => { + return ( + + {!has5Tags && ( + + )} + {!has5Tags && Object.values(fields)} + + ) + }} + + {hasPendingTags && ( + + + + + + + + + + + + + + + + + )} + + ) +} + +function HoldingTime({ created }: { created: Date }) { + const expires = useMemo(() => { + // expires 30 minutes after creation + return new Date(created.getTime() + 1000 * 60 * 30) + }, [created]) + const { minutes, seconds, diffInMs } = useTimeRemaining(expires) + if (diffInMs <= 0) return 'Claimable' + + return `${minutes} m ${seconds} s` +} + +function ConfirmTagPrice({ tag }: { tag: { name: string } }) { + const price = useMemo(() => tagLengthToWei(tag?.name.length), [tag]) + + return `${formatEther(price).toLocaleString()} ETH` +} + +function TotalPrice() { + const pendingTags = usePendingTags() + + const weiAmount = useMemo(() => getPriceInWei(pendingTags ?? []), [pendingTags]) + + return ( + + + Total + + + {formatEther(weiAmount).toLocaleString()} ETH + + + ) +} diff --git a/packages/app/features/account/sendtag/checkout/checkout-utils.tsx b/packages/app/features/account/sendtag/checkout/checkout-utils.tsx new file mode 100644 index 000000000..7fc1781b5 --- /dev/null +++ b/packages/app/features/account/sendtag/checkout/checkout-utils.tsx @@ -0,0 +1,85 @@ +import type { Tables } from '@my/supabase/database.types' +import { type baseMainnetClient, sendRevenueSafeAddress } from '@my/wagmi' +import { parseEther } from 'viem' + +export const verifyAddressMsg = (a: string | `0x${string}`) => + `I am the owner of the address: ${a}. + +Send.it` + +//@todo: should probaby fetch this from db +export const maxNumSendTags = 5 + +export function getPriceInWei(pendingTags: { name: string }[]) { + return pendingTags.reduce((acc, { name }) => { + const total = acc + tagLengthToWei(name.length) + + return total + }, BigInt(0)) +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function tagLengthToWei(length: number) { + switch (length) { + case 5: + return parseEther('0.005') + case 4: + return parseEther('0.01') + case 3: + case 2: + case 1: + return parseEther('0.02') + default: + return parseEther('0.002') + } +} + +export async function getSenderSafeReceivedEvents({ + publicClient, + sender, +}: { + publicClient: typeof baseMainnetClient + sender: `0x${string}` +}) { + const fromBlock = { + [8453]: BigInt(11269822), // base mainnet send revenue contract creation block + [845337]: BigInt(11269822), // base mainnet fork send revenue contract creation block + [84532]: BigInt(7469197), // base sepolia send revenue contract creation block + }[publicClient.chain.id] + return await publicClient.getLogs({ + event: { + type: 'event', + inputs: [ + { + name: 'sender', + internalType: 'address', + type: 'address', + indexed: true, + }, + { + name: 'value', + internalType: 'uint256', + type: 'uint256', + indexed: false, + }, + ], + name: 'SafeReceived', + }, + address: sendRevenueSafeAddress[publicClient.chain.id], + args: { + sender, + }, + strict: true, + fromBlock, + }) +} + +export const hasFreeTag = (tagName: string, confirmedTags: Tables<'tags'>[]) => { + // could be free if tag name is greater than 6 characters + const hasFreeTag = tagName.length >= 6 + + // check if there are any confirmed tags that are 6 characters or longer + return ( + hasFreeTag && (confirmedTags?.length === 0 || confirmedTags.every((tag) => tag.name.length < 6)) + ) +} diff --git a/packages/app/features/account/sendtag/checkout/components/checkout-confirm-button.tsx b/packages/app/features/account/sendtag/checkout/components/checkout-confirm-button.tsx new file mode 100644 index 000000000..32f8f6f07 --- /dev/null +++ b/packages/app/features/account/sendtag/checkout/components/checkout-confirm-button.tsx @@ -0,0 +1,447 @@ +import { + Anchor, + ButtonIcon, + Spinner, + Tooltip, + type ButtonProps, + useMedia, + YStack, + Paragraph, + Button, + ButtonText, +} from '@my/ui' + +import { AlertTriangle, CheckCircle } from '@tamagui/lucide-icons' +import { useEffect, useMemo, useState } from 'react' +import { getPriceInWei, verifyAddressMsg } from '../checkout-utils' +import { + useAccount, + useBalance, + useConnect, + useEstimateGas, + usePublicClient, + useSendTransaction, + useSignMessage, + useWaitForTransactionReceipt, +} from 'wagmi' +import { baseMainnetClient, sendRevenueSafeAddress } from '@my/wagmi' +import { assert } from 'app/utils/assert' +import { api } from 'app/utils/api' +import { shorten } from 'app/utils/strings' +import { TRPCClientError } from '@trpc/client' +import { useReceipts } from 'app/utils/useReceipts' +import { useConnectModal, useChainModal } from '@rainbow-me/rainbowkit' +import { usePendingTags } from 'app/utils/tags' +import { useUser } from 'app/utils/useUser' +import { useChainAddresses } from 'app/utils/useChainAddresses' + +export function ConfirmButton({ + onConfirmed, + needsVerification, +}: { + onConfirmed: () => void + needsVerification: boolean +}) { + const media = useMedia() + const { updateProfile } = useUser() + + //Connect + const pendingTags = usePendingTags() + + const { chainId } = useAccount() + const { openConnectModal } = useConnectModal() + const { openChainModal } = useChainModal() + const { error: connectError } = useConnect() + + //Verify + const publicClient = usePublicClient() + + const { address: connectedAddress } = useAccount() + const { data: ethBalance } = useBalance({ + address: connectedAddress, + chainId: baseMainnetClient.chain.id, + }) + + const weiAmount = useMemo(() => getPriceInWei(pendingTags ?? []), [pendingTags]) + + const canAffordTags = ethBalance && ethBalance.value >= weiAmount + + // Confirm + const [submitting, setSubmitting] = useState(false) + const confirm = api.tag.confirm.useMutation() + const { refetch: refetchReceipts } = useReceipts() + const [sentTx, setSentTx] = useState<`0x${string}`>() + + const { + data: txReceipt, + isLoading: txWaitLoading, + error: txWaitError, + } = useWaitForTransactionReceipt({ + hash: sentTx, + confirmations: 2, + }) + + const [error, setError] = useState() + const [confirmed, setConfirmed] = useState(false) + + const [attempts, setAttempts] = useState(0) + + const { sendTransactionAsync } = useSendTransaction({ + mutation: { + onSuccess: setSentTx, + }, + }) + + const tx = { + to: sendRevenueSafeAddress[chainId as keyof typeof sendRevenueSafeAddress], + chainId: baseMainnetClient.chain.id, + value: weiAmount, + } as const + + const { data: txData, error: estimateGasErr } = useEstimateGas({ + ...tx, + query: { + enabled: chainId !== undefined && canAffordTags, + }, + }) + + function handleCheckoutTx() { + assert(!!sendTransactionAsync, 'sendTransactionAsync is required') + sendTransactionAsync({ + gas: txData, + ...tx, + }).catch((err) => { + console.error(err) + setError('Something went wrong') + }) + } + + function submitTxToDb(tx: string) { + setAttempts((a) => a + 1) + setSubmitting(true) + confirm + .mutateAsync({ transaction: tx }) + .then(async () => { + setConfirmed(true) + setSubmitting(false) + // Is this comment still applicable after refactor? + // FIXME: these prob should be passed in as props since the portal and app root do not share the same providers + await updateProfile().then(() => { + refetchReceipts() + }) + onConfirmed() + }) + .catch((err) => { + if (err instanceof TRPCClientError) { + // handle transaction too new error + if ( + [ + 'Transaction too new.', + 'The Transaction may not be processed on a block yet.', + `Transaction with hash "${tx}" could not be found`, + ].some((s) => err.message.includes(s)) && + attempts < 10 + ) { + // try again + setTimeout(() => { + submitTxToDb(tx) + }, 1000) + return + } + + setError(err.message) + return + } + console.error(err) + setError('Something went wrong') + }) + } + + useEffect(() => { + if (txReceipt) submitTxToDb(txReceipt.transactionHash) + }, [txReceipt]) + + useEffect(() => { + if (txWaitError) { + setError(txWaitError.message) + } + }, [txWaitError]) + + const { signMessageAsync } = useSignMessage({ + mutation: { + onError: (err) => { + setError('details' in err ? err?.details : err?.message) + }, + }, + }) + + const { + data: addresses, + isLoading: isLoadingAddresses, + isRefetching: isRefetchingAddresses, + refetch: updateAddresses, + } = useChainAddresses() + + const [savedAddress, setSavedAddress] = useState(addresses?.[0]?.address) //this only works cause we limit to 1 address\ + + useEffect(() => { + if (isLoadingAddresses || addresses?.length === 0) return + setSavedAddress(addresses?.[0]?.address) + }, [isLoadingAddresses, addresses]) + + const verify = api.chainAddress.verify.useMutation() + + function handleVerify() { + assert(!!signMessageAsync, 'signMessageAsync is required') + assert(!!connectedAddress, 'connectedAddress is required') + signMessageAsync({ message: verifyAddressMsg(connectedAddress) }) + .then((signature) => verify.mutateAsync({ address: connectedAddress, signature })) + .then(() => updateAddresses()) + .then(({ data: addresses }) => { + setSavedAddress(addresses?.[0].address) + }) + .catch((e) => { + if (e instanceof TRPCClientError) { + setError(e.message) + } else { + console.error(e) + setError('Something went wrong') + } + }) + } + + // @TODO: This is not native compatible + //We will need to seperate this logic so that we can switch between web and native + if (connectError?.message) { + return ( + + + + {connectError?.message} + + + + ) + } + + // @TODO: This is not native compatible + // We will need to seperate this logic so that we can switch between web and native + if (!connectedAddress) { + return ( + + ) + } + + if (chainId !== baseMainnetClient.chain.id) { + return ( + + + + Please switch to Base to confirm your Send Tags. + + + + ) + } + + if ((!savedAddress && !isRefetchingAddresses) || needsVerification) { + return ( + + + + Please verify your wallet to confirm your Send Tags. + + + + ) + } + + if (isRefetchingAddresses) { + return ( + <> + + Checking your wallet address... + + + + ) + } + + if (savedAddress && savedAddress !== connectedAddress) { + return ( + + + + Please switch to the wallet address you verified earlier. + + + {shorten(savedAddress)} + + + + ) + } + + if (error) { + return ( + { + switch (true) { + case needsVerification || !savedAddress || connectedAddress !== savedAddress: + handleVerify() + break + case !txReceipt || txWaitError !== null: + handleCheckoutTx() + break + default: + submitTxToDb(txReceipt?.transactionHash) + break + } + }} + > + + + {error} + + + + ) + } + + if (pendingTags?.length === 0 && !confirmed) { + return ( + + + + + You have no Send Tags to confirm. Please add some Send Tags. + + + + ) + } + + return ( + + ) +} + +//@todo this error tooltip can be used in other places. Abstact it up the tree +const ConfirmButtonError = ({ + children, + onPress, + buttonText, + ...props +}: ButtonProps & { buttonText?: string }) => { + const media = useMedia() + return ( + + + + {children} + + + + + + ) +} diff --git a/packages/app/features/checkout/screen.test.tsx b/packages/app/features/account/sendtag/checkout/screen.test.tsx similarity index 100% rename from packages/app/features/checkout/screen.test.tsx rename to packages/app/features/account/sendtag/checkout/screen.test.tsx diff --git a/packages/app/features/account/sendtag/checkout/screen.tsx b/packages/app/features/account/sendtag/checkout/screen.tsx new file mode 100644 index 000000000..0d122374a --- /dev/null +++ b/packages/app/features/account/sendtag/checkout/screen.tsx @@ -0,0 +1,30 @@ +import { Container, Stack } from '@my/ui' +import { useConfirmedTags } from 'app/utils/tags' +import { useEffect } from 'react' +import { useRouter } from 'solito/router' +import { CheckoutForm } from './checkout-form' + +export const CheckoutScreen = () => { + const confirmedTags = useConfirmedTags() + const router = useRouter() + + useEffect(() => { + if (confirmedTags?.length === 5) { + router.replace('/account/sendtag') + } + }, [confirmedTags, router]) + + return ( + + + + + + ) +} diff --git a/packages/app/features/account/sendtag/screen.tsx b/packages/app/features/account/sendtag/screen.tsx new file mode 100644 index 000000000..c92c2555e --- /dev/null +++ b/packages/app/features/account/sendtag/screen.tsx @@ -0,0 +1,136 @@ +import type { Tables } from '@my/supabase/database.types' +import { + Container, + H3, + Label, + ListItem, + Paragraph, + Spinner, + Stack, + YStack, + Button, + ButtonText, +} from '@my/ui' + +import { useUser } from 'app/utils/useUser' +import { maxNumSendTags } from './checkout/checkout-utils' + +import { IconPlus } from 'app/components/icons' +import { useRouter } from 'solito/router' + +export function SendTagScreen() { + const { tags, isLoading } = useUser() + const confirmedTags = tags?.filter((tag) => tag.status === 'confirmed') + + const allTags: (Tables<'tags'> | undefined)[] = + confirmedTags === undefined + ? new Array(maxNumSendTags).fill(undefined) + : [...confirmedTags, ...Array.from({ length: maxNumSendTags - confirmedTags.length })] + + if (isLoading) + return ( + + + + ) + + return ( + + + + + + + + + ) +} + +function SendtagList({ + allTags, +}: { + allTags: (Tables<'tags'> | undefined)[] + + confirmedTags?: Tables<'tags'>[] +}) { + const nextTagIndex = allTags?.findIndex((tag) => tag === undefined) + + return ( + + {allTags.map((tag, i) => + i === nextTagIndex ? ( + + ) : ( + + ) + )} + + ) +} + +function AddTagButton() { + const { push } = useRouter() + return ( + + ) +} + +function TagItem({ tag }: { tag?: Tables<'tags'> }) { + if (tag === undefined) + return ( + + ) + + return ( + +

+ {tag.name} +

+
+ ) +} diff --git a/packages/app/features/account/settings/SettingsBottomSheet.tsx b/packages/app/features/account/settings/SettingsBottomSheet.tsx new file mode 100644 index 000000000..39bf7223c --- /dev/null +++ b/packages/app/features/account/settings/SettingsBottomSheet.tsx @@ -0,0 +1,47 @@ +import { Button, Nav, Paragraph, ScrollView, type SheetProps, XStack, YStack } from '@my/ui' + +import { useNav } from 'app/routers/params' +import { IconX } from 'app/components/icons' +import { SettingsLinks } from './SettingsLinks' +import { NavSheet } from 'app/components/NavSheet' + +export const SettingsBottomSheet = ({ open }: SheetProps) => { + const [nav, setNavParam] = useNav() + + const onOpenChange = () => { + if (open) setNavParam('settings', { webBehavior: 'replace' }) + else setNavParam(undefined, { webBehavior: 'replace' }) + } + + return ( + + + Settings + + + + + + - - ) -} - -const SignInButtons = () => { - const { setCarouselProgress } = useContext(AuthCarouselContext) - return ( - - - - - ) -} - -const carouselItems = [ - { - title: 'LIKE CASH', - description: 'SEND AND RECEIVE MONEY GLOBALLY IN SECONDS', - }, - { - title: 'ALL YOURS', - description: 'ONLY YOU HAVE ACCESS TO YOUR FUNDS', - }, - { - title: 'SECURE', - description: 'PRIVACY FIRST WITH VERFIED SIGN-IN AND TRANSFERS', - }, -] - -const CarouselProgress = () => { - const { carouselProgress, setCarouselProgress } = useContext(AuthCarouselContext) - const [progressWidth, setProgressWidth] = useState(0) - - useEffect(() => { - const progressWidthInterval = setInterval(() => { - setProgressWidth((progressWidth) => { - return progressWidth >= 100 ? 0 : progressWidth + 1 - }) - if (progressWidth >= 100) { - setCarouselProgress((progress) => (progress + 1) % carouselItems.length) - } - }, 50) - - return () => { - clearInterval(progressWidthInterval) - } - }, [setCarouselProgress, progressWidth]) - - return ( - - {carouselItems.map(({ title }, i) => { - return ( - - - - ) - })} - - ) -} - -const SignInCarousel = () => { - const { carouselProgress } = useContext(AuthCarouselContext) - const { gtMd } = useMedia() - - const item = carouselItems.at(carouselProgress) - - return ( - - -

- {item?.title} -

- - {item?.description} - -
- {gtMd ? ( - - ) : carouselProgress < carouselItems.length - 1 ? ( - - ) : ( - - )} -
- ) -} diff --git a/packages/app/features/auth/sign-in/screen.tsx b/packages/app/features/auth/sign-in/screen.tsx new file mode 100644 index 000000000..79f08bfd6 --- /dev/null +++ b/packages/app/features/auth/sign-in/screen.tsx @@ -0,0 +1,116 @@ +import { Stack, YStack, Button, ButtonText, XStack, useMedia, Theme } from '@my/ui' +import { IconSendLogo } from 'app/components/icons' +import { useContext, useEffect, useState } from 'react' +import { SignInForm } from 'app/features/auth/sign-in/sign-in-form' + +import { AuthCarouselContext } from 'app/features/auth/AuthCarouselContext' +import { Carousel } from 'app/features/auth/components/Carousel' + +export const SignInScreen = () => { + const { carouselProgress } = useContext(AuthCarouselContext) + const media = useMedia() + + if (media.gtMd) + return ( + + + + + + ) + + return ( + + + + ) +} + +const screens = ['screen1', 'screen2', 'screen3', 'form'] as const + +const SignInScreensMobile = () => { + const [signInProgress, setSignInProgress] = useState(0) + const { setCarouselProgress } = useContext(AuthCarouselContext) + + useEffect(() => { + setCarouselProgress(0) + }, [setCarouselProgress]) + + const nextScreen = () => { + setSignInProgress((progress) => { + setCarouselProgress(progress + 1) + return progress + 1 + }) + } + + const getSignInButtons = (page: (typeof screens)[number] | undefined) => { + switch (true) { + case page === 'screen1' || page === 'screen2': + return + case page === 'screen3': + return + case page === 'form': + return null + default: + return + } + } + + return ( + <> + + {screens[signInProgress] === 'form' ? ( + + + + ) : ( + + )} + + + {screens[signInProgress] === 'form' ? ( + + ) : ( + <> + + + {getSignInButtons(screens[signInProgress])} + + )} + + + ) +} + +const ContinueButton = ({ nextScreen }: { nextScreen: () => void }) => ( + + + +) + +const SignInButtons = ({ nextScreen }: { nextScreen: () => void }) => ( + + + + +) diff --git a/packages/app/features/auth/sign-in-form.tsx b/packages/app/features/auth/sign-in/sign-in-form.tsx similarity index 56% rename from packages/app/features/auth/sign-in-form.tsx rename to packages/app/features/auth/sign-in/sign-in-form.tsx index 8b998df3d..d0620cd27 100644 --- a/packages/app/features/auth/sign-in-form.tsx +++ b/packages/app/features/auth/sign-in/sign-in-form.tsx @@ -1,14 +1,14 @@ -import { ButtonText, H1, H3, Paragraph, SubmitButton, Theme, XStack, YStack } from '@my/ui' +import { ButtonText, BigHeading, Paragraph, SubmitButton, XStack, YStack, H3 } from '@my/ui' import { SchemaForm, formFields } from 'app/utils/SchemaForm' import { FormProvider, useForm } from 'react-hook-form' import { api } from 'app/utils/api' import { useRouter } from 'solito/router' -import { VerifyCode } from './components/VerifyCode' +import { VerifyCode } from 'app/features/auth/components/VerifyCode' import { z } from 'zod' const SignInSchema = z.object({ countrycode: formFields.countrycode, - phone: formFields.text, + phone: formFields.text.min(1).max(20), }) export const SignInForm = () => { const form = useForm>() @@ -54,53 +54,80 @@ export const SignInForm = () => { countrycode: { // @ts-expect-error unsure how to get web props to work with tamagui 'aria-label': 'Country Code', - height: '$3', + size: '$3', }, phone: { 'aria-label': 'Phone number', - borderBottomColor: '$accent9Light', + '$theme-dark': { + borderBottomColor: '$accent10Dark', + }, + '$theme-light': { + borderBottomColor: '$accent9Light', + }, + fontFamily: '$mono', + fontVariant: ['tabular-nums'], + fontSize: '$7', borderWidth: 0, borderBottomWidth: 2, borderRadius: '$0', - placeholder: 'Phone number', width: '100%', backgroundColor: 'transparent', + color: '$color12', outlineColor: 'transparent', + theme: 'accent', + focusStyle: { + borderBottomColor: '$accent3Light', + }, }, }} renderAfter={({ submit }) => ( - + submit()} br="$3" bc={'$accent9Light'} - w={'$12'} - $sm={{ dsp: form.getValues().phone?.length > 0 ? 'flex' : 'none' }} + $sm={{ w: '100%' }} + $gtMd={{ + mt: '0', + als: 'flex-end', + mx: 0, + ml: 'auto', + w: '$10', + h: '$3.5', + }} > - - {'/SEND IT!'} + + {'/SEND IT'} )} > {(fields) => ( - - -

- WELCOME TO SEND -

-
-

- Sign up or Sign in with your phone number + + WELCOME TO SEND +

+ Sign up or Sign in with your phone number.

- - - - Your Phone - - - {Object.values(fields)} + + + + Your Phone + + {Object.values(fields)} )} diff --git a/packages/app/features/checkout/components/confirm-dialog.tsx b/packages/app/features/checkout/components/confirm-dialog.tsx deleted file mode 100644 index d065cc13e..000000000 --- a/packages/app/features/checkout/components/confirm-dialog.tsx +++ /dev/null @@ -1,901 +0,0 @@ -import { - Adapt, - Anchor, - AnimatePresence, - Button, - Dialog, - Fieldset, - Label, - Paragraph, - ScrollView, - Sheet, - Spinner, - Theme, - TooltipSimple, - Unspaced, - XStack, - YStack, - YStackProps, -} from '@my/ui' -import { sendRevenueSafeAddress } from '@my/wagmi' -import { CheckCircle, X } from '@tamagui/lucide-icons' -import { TRPCClientError } from '@trpc/client' -import { api } from 'app/utils/api' -import { getXPostHref } from 'app/utils/getReferralLink' -import { shorten } from 'app/utils/strings' -import { useConfirmedTags, usePendingTags } from 'app/utils/tags' -import { useChainAddresses } from 'app/utils/useChainAddresses' -import { useMounted } from 'app/utils/useMounted' -import { useReceipts } from 'app/utils/useReceipts' -import { useUser } from 'app/utils/useUser' -import { useRpcChainId } from 'app/utils/viem/useRpcChainId' -import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react' -import { useLink } from 'solito/link' -import { type PublicClient, formatEther } from 'viem' -import { - useAccount, - useBlockNumber, - useConnect, - useEstimateGas, - usePublicClient, - useSendTransaction, - useSignMessage, - useSwitchChain, - useWaitForTransactionReceipt, -} from 'wagmi' -import { getPriceInWei, getSenderSafeReceivedEvents, verifyAddressMsg } from '../screen' -import { assert } from 'app/utils/assert' - -export interface ConfirmContextType { - open: boolean - closeable: boolean - setCloseable: (closeable: boolean) => void - onConfirmed: () => void -} - -export const ConfirmContext = createContext( - null as unknown as ConfirmContextType -) - -const Provider = ({ - children, - closeable, - setCloseable, - open, - onConfirmed, -}: { children: React.ReactNode } & ConfirmContextType) => { - return ( - - {children} - - ) -} - -export const useConfirmContext = () => { - const ctx = useContext(ConfirmContext) - if (!ctx) { - throw new Error('useConfirmContext must be used within a ConfirmContext.Provider') - } - return ctx -} - -export function ConfirmDialog({ - onConfirmed, - needsVerification, -}: { - onConfirmed: () => void - needsVerification: boolean -}) { - const pendingTags = usePendingTags() - const confirmedTags = useConfirmedTags() - const hasPendingTags = pendingTags && pendingTags?.length > 0 - const [open, setOpen] = useState(false) - const [closeable, setCloseable] = useState(true) - const handleOpenChange = (open: boolean) => { - if (!closeable && !open) return - setOpen(open) - } - const weiAmount = useMemo( - () => getPriceInWei(pendingTags ?? [], confirmedTags ?? []), - [pendingTags, confirmedTags] - ) - const ethAmount = useMemo(() => formatEther(weiAmount), [weiAmount]) - - // when not closeable prevent browser navigation - useEffect(() => { - if (!closeable) { - const handleBeforeUnload = (e: BeforeUnloadEvent) => { - if (confirm('You have a pending Send Tag transaction. Are you sure you want to leave?')) { - return - } - e.preventDefault() - e.returnValue = '' - return - } - window.addEventListener('beforeunload', handleBeforeUnload) - return () => { - window.removeEventListener('beforeunload', handleBeforeUnload) - } - } - }, [closeable]) - - return ( - - - {(hasPendingTags || needsVerification) && ( - - - {BigInt(weiAmount) > 0 && ( - - Total: {ethAmount} ETH - - )} - - - - - - )} - - - - - - - - - ) -} - -export function ConfirmFlow() { - const { isConnected, chainId } = useAccount() - const { connect, connectors, error: connectError } = useConnect() - const publicClient = usePublicClient() - const { data: rpcChainId, isLoading: isLoadingRpcChainId } = useRpcChainId() - - assert(!!publicClient?.chain.id, 'publicClient.chain.id is required') - - const { switchChain } = useSwitchChain() - const { isLoadingTags } = useUser() - - if (isLoadingTags) { - return ( - - Checking your Send Tags... - - - ) - } - - if (!isLoadingRpcChainId && rpcChainId !== publicClient.chain.id) { - return ( - - - 😵 Tell a dev! This should not happen. RPC chain id {rpcChainId} does not match public - client chain id: {publicClient.chain.id}. - - - ) - } - - if (!isConnected) { - return ( - - - Please connect your wallet to confirm your Send Tags. - - - - {connectError && ( - - - {connectError.message.includes('Connector not found') - ? 'Please install a web3 wallet like MetaMask.' - : connectError.message} - - - )} - - - ) - } - - if (publicClient.chain.id !== chainId) { - return ( - - - Please switch to {publicClient.chain.name} in your wallet. - - - - - - ) - } - - return -} - -export function ConfirmWithVerifiedAddress() { - const publicClient = usePublicClient() - const verify = api.chainAddress.verify.useMutation() - const { - data: addresses, - isLoading: isLoadingAddresses, - refetch: updateAddresses, - } = useChainAddresses() - const { address: connectedAddress, status } = useAccount() - const { signMessageAsync, error: signMsgErr } = useSignMessage() - const address = addresses?.[0]?.address // this only works cause we limit to 1 address - const savedAddress = useMemo(() => address, [address]) - const [error, setError] = useState() - - useEffect(() => { - if (status === 'connected' || status === 'disconnected') { - setError(undefined) - } - }, [status]) - - if (isLoadingAddresses || !connectedAddress) { - return ( - - Checking your wallet address... - - - ) - } - - if (savedAddress && savedAddress !== connectedAddress) { - return ( - - Your account already has a verified address. - - - Please switch to the wallet address{' '} - - {shorten(savedAddress)} - {' '} - you verified earlier. - - - - ) - } - - if (!savedAddress) { - // ensure the user has verified their address - return ( - - - Please press Sign Message to verify your wallet address,{' '} - - {shorten(connectedAddress)}. - - - - You can only verify one wallet address and it cannot be changed for now. - - - - - - - {(signMsgErr || error) && ( - - - {error ? `${error} ` : ''} - {signMsgErr && 'details' in signMsgErr ? signMsgErr?.details : signMsgErr?.message} - - - )} - - - ) - } - - return -} - -export function ConfirmWithSignTransaction() { - const { isLoadingTags, updateProfile, profile } = useUser() - const pendingTags = usePendingTags() - const confirmedTags = useConfirmedTags() - const { refetch: refetchReceipts } = useReceipts() - const ethAmount = getPriceInWei(pendingTags ?? [], confirmedTags ?? []) - const confirm = api.tag.confirm.useMutation() - const publicClient = usePublicClient() - const [sentTx, setSentTx] = useState<`0x${string}`>() - const { data: txReceipt, error: txWaitError } = useWaitForTransactionReceipt({ - hash: sentTx, - confirmations: 2, - }) - const [submitted, setSubmitted] = useState(false) - const [error, setError] = useState() - const [confirmed, setConfirmed] = useState(false) - const isFree = ethAmount === BigInt(0) - const paidOrFree = (pendingTags ?? []).length > 0 && (isFree || txReceipt) - const mounted = useMounted() - const { open, setCloseable, onConfirmed } = useConfirmContext() - const referralLink = useLink({ - href: getXPostHref(profile?.referral_code ?? ''), - }) - const [attempts, setAttempts] = useState(0) - const reset = useCallback(() => { - setConfirmed(false) - setSubmitted(false) - setSentTx(undefined) - setError(undefined) - confirm.reset() - }, [confirm]) - - // handle sending confirmation to the server - useEffect(() => { - if (!mounted) return - if (submitted) return - if (confirmed) return - if (isLoadingTags) return - if (!isFree && !sentTx) return - if (!pendingTags || pendingTags.length === 0) return - if (confirm.isPending) return - if (paidOrFree) { - setSubmitted(true) - setAttempts((a) => a + 1) - confirm - .mutateAsync(isFree ? {} : { transaction: sentTx }) - .then(async () => { - setConfirmed(true) - setCloseable(true) - // FIXME: these prob should be passed in as props since the portal and app root do not share the same providers - await updateProfile().then(() => { - refetchReceipts() - }) - onConfirmed() - }) - .catch((err) => { - if (err instanceof TRPCClientError) { - // handle transaction too new error - if ( - [ - 'Transaction too new.', - 'The Transaction may not be processed on a block yet.', - `Transaction with hash "${sentTx}" could not be found`, - ].some((s) => err.message.includes(s)) && - attempts < 10 - ) { - // try again - setTimeout(() => { - reset() - }, 1000) - return - } - setError(err.message) - return - } - console.error(err) - setError('Something went wrong') - }) - } - }, [ - mounted, - confirmed, - paidOrFree, - submitted, - isLoadingTags, - isFree, - sentTx, - confirm, - updateProfile, - pendingTags, - setCloseable, - onConfirmed, - refetchReceipts, - attempts, - reset, - ]) - - // biome-ignore lint/correctness/useExhaustiveDependencies: run when confirmed and submitted changes - useEffect(() => { - if (confirmed) { - setCloseable(true) - } - }, [confirmed, submitted, setCloseable]) - - // reset state when closing - useEffect(() => { - if (!open) { - reset() - } - }, [open, reset]) - - assert(!!publicClient, 'publicClient is required') - - if (confirmed) { - return ( - - Send Tags are confirmed. - {txReceipt && ( - - - Confirmed transaction using{' '} - {publicClient.chain.blockExplorers ? ( - - {shorten(txReceipt.transactionHash, 7, 3)} - - ) : ( - shorten(txReceipt.transactionHash, 7, 3) - )} - - - )} - - - ) - } - - // show confirming and loading until hooks run - if (pendingTags === undefined || isLoadingTags) { - return ( - - - Pardon the interruption, we are loading your Send Tags... - - - - ) - } - - if (pendingTags?.length === 0 && !confirmed) { - return ( - - - You have no Send Tags to confirm. Please add some Send Tags. - - - ) - } - - if (!isFree && !sentTx) { - return ( - { - if (tx) setCloseable(false) // don't allow closing while waiting for tx - setSentTx(tx) - }} - /> - ) - } - - return ( - - - {!isFree && ( - - Sent transaction...{' '} - {publicClient.chain.blockExplorers ? ( - - {shorten(sentTx, 7, 3)} - - ) : ( - shorten(sentTx, 7, 3) - )} - - )} - {!error && ( - - {!confirm.isPending && Awaiting transaction confirmation...} - {submitted && confirm.isPending && Confirming Send Tags...} - - - )} - - - {error && ( - - - {error} {txWaitError?.message} - - - - )} - - - ) -} - -export function ConfirmSendTransaction({ onSent }: { onSent: (tx: `0x${string}`) => void }) { - const publicClient = usePublicClient() - const { address } = useAccount() - const { receipts, error: errorReceipts, isLoading: isLoadingReceipts } = useReceipts() - const pendingTags = usePendingTags() - const confirmedTags = useConfirmedTags() - const ethAmount = getPriceInWei(pendingTags ?? [], confirmedTags ?? []) - - const tx = { - to: sendRevenueSafeAddress, - value: ethAmount, - } as const - const { data: txData, error: sendTxErr, isLoading } = useEstimateGas(tx) - const { sendTransactionAsync } = useSendTransaction() - const [error, setError] = useState() - const receiptHashes = useMemo(() => receipts?.map((r) => r.hash) ?? [], [receipts]) - const { data: block } = useBlockNumber() - - assert(!!publicClient, 'publicClient is required') - - const lookupSafeReceivedEvent = useCallback(async () => { - if (!address) return - if (isLoadingReceipts) return - if (receipts === undefined) return - const events = await getSenderSafeReceivedEvents({ - publicClient: publicClient as PublicClient, - sender: address, - }) - const event = events.filter( - (e) => e.args.value === ethAmount && !receiptHashes.includes(e.transactionHash) - )?.[0] - // check it against the receipts - if (event?.transactionHash) { - onSent(event.transactionHash) - } - }, [receipts, publicClient, address, ethAmount, onSent, isLoadingReceipts, receiptHashes]) - - // watch for new receipts - // biome-ignore lint/correctness/useExhaustiveDependencies: run when block changes - useEffect(() => { - lookupSafeReceivedEvent() - }, [block, lookupSafeReceivedEvent]) - - return ( - - - Press Sign Transaction below to confirm your Send Tags and sign the transaction in your - wallet. - -
- - - - {shorten(address)} - - -
-
- - - - Send Tag Safe - - -
-
- - {formatEther(ethAmount).toLocaleString()} ETH -
- - - - {!!(sendTxErr || error || errorReceipts) && ( - - - {error ? `${error} ` : ''} - {errorReceipts instanceof Error ? `${errorReceipts?.message} ` : ''} - {sendTxErr && 'details' in sendTxErr ? sendTxErr?.details : sendTxErr?.message} - - - )} - -
- ) -} - -export function ConfirmCloseDialog() { - const { closeable } = useConfirmContext() - return ( - - {closeable && ( - - - - - - - - - -
- ))} - - - )} - > - {(fields) => { - return ( - - - -

Send Tags

- -
- - - - {hasConfirmedTags && ( - Send Tags Registered - )} - - - {confirmedTags?.map((tag) => ( - - {tag.name} - - ))} - {hasConfirmedTags && } - - - Pick a unique a Send Tag that is not yet reserved. Reserve up to 5 tags. - - - - {!hasPendingTags && !hasConfirmedTags && ( - You have no Send Tags yet - )} - {hasPendingTags && ( - - Your Send Tags are not confirmed until payment is received and your wallet - is verified. - - )} - {isTouch && ( - - Each Send Tag is reserved for 30 minutes. If you do not claim it within - that time, it is claimable by someone else. - - )} - -
- - - Registered {user?.tags?.length ?? 0} / 5 - - {!has5Tags && Object.values(fields)} - -
- ) - }} - - - - - - - - - - ) -} - -function HoldingTime({ created }: { created: Date }) { - const expires = useMemo(() => { - // expires 30 minutes after creation - return new Date(created.getTime() + 1000 * 60 * 30) - }, [created]) - const { minutes, seconds, diffInMs } = useTimeRemaining(expires) - - return ( - - - {diffInMs <= 0 && ( - - - - Claimable - - - )} - {diffInMs > 0 && ( - - - - - {minutes}m {seconds}s - - - - )} - - - - - Each Send Tag is reserved for 30 minutes. If you do not claim it within that time, it is - claimable by someone else. - - - - - ) -} - -function ConfirmTagPrice({ tag }: { tag: { name: string } }) { - const confirmedTags = useConfirmedTags() ?? [] - const pendingTags = usePendingTags() ?? [] - const commonTags = pendingTags.filter((t) => t.name.length >= 6) - - // could be free if tag name is greater than 6 characters - let hasFreeTag = tag.name.length >= 6 - - // check if there are any confirmed tags that are 6 characters or longer - hasFreeTag = - hasFreeTag && (confirmedTags?.length === 0 || confirmedTags.every((tag) => tag.name.length < 6)) - - // this tag is free if it's the first tag greater than 6 characters - hasFreeTag = hasFreeTag && commonTags[0]?.name === tag.name - - const price = useMemo(() => tagLengthToWei(tag?.name.length, hasFreeTag), [tag, hasFreeTag]) - - return price === BigInt(0) ? ( - Free - ) : ( - Price: {formatEther(price).toLocaleString()} ETH - ) -} - -function SendTagPricingDialog() { - return ( - - - - - - - - - - - - - - - - - - - Send Tag Pricing - - Send Tags are priced based on their length. The shorter the Send Tag, the more it costs. - - -
- - - - 6+ characters - - - - First one {(0.005).toLocaleString()} ETH, {(0.01).toLocaleString()} ETH after - - - - - - 5 characters - - - {(0.01).toLocaleString()} ETH - - - - - 4 characters - - - {(0.03).toLocaleString()} ETH - - - - - 1-3 characters - - - {(0.05).toLocaleString()} ETH - - - -
-
- - - - - - - - - -
- ) -} diff --git a/packages/app/features/distributions/components/DistributionStepImages.tsx b/packages/app/features/distributions/components/DistributionStepImages.tsx deleted file mode 100644 index f633ce43b..000000000 --- a/packages/app/features/distributions/components/DistributionStepImages.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { Image, ImageProps } from '@my/ui' - -import step1Img from 'app/assets/img/onboarding/connect-wallet.png' -import step2Img from 'app/assets/img/onboarding/send-tag.png' -import step3Img from 'app/assets/img/onboarding/sign.png' - -const DistributionStepImage = (props: ImageProps) => ( - -) - -export const DistributionConnectImage = () => ( - -) -export const DistributionRegisterImage = () => ( - -) -export const DistributionSignImage = () => diff --git a/packages/app/features/distributions/components/DistributionsTable.tsx b/packages/app/features/distributions/components/DistributionsTable.tsx deleted file mode 100644 index ed6494f85..000000000 --- a/packages/app/features/distributions/components/DistributionsTable.tsx +++ /dev/null @@ -1,363 +0,0 @@ -import { - Accordion, - Anchor, - Card, - H4, - H6, - KVTable, - Paragraph, - Separator, - SizableText, - Spinner, - Square, - TooltipSimple, - XStack, - YStack, -} from '@my/ui' -import { PostgrestError } from '@supabase/supabase-js' -import { ChevronDown } from '@tamagui/lucide-icons' -import { - UseDistributionsResultData, - useDistributions, - useSendSellCountDuringDistribution, -} from 'app/utils/distributions' -import formatAmount from 'app/utils/formatAmount' -import { shorten } from 'app/utils/strings' -import { useChainAddresses } from 'app/utils/useChainAddresses' -import { useSendBalance, useSendBalanceOfAt } from 'app/utils/useSendBalance' -import { useTimeRemaining } from 'app/utils/useTimeRemaining' -import { formatUnits } from 'viem' -import { DistributionClaimButton } from './DistributionClaimButton' -import { DistributionsStatCard } from './distribution-stat-cards' - -export const DistributionsTable = () => { - const { data: distributions, isLoading, error } = useDistributions() - - if (isLoading) return Loading... - if (error) - return Error loading distributions: {(error as PostgrestError).message} - - return ( - - {distributions - ?.sort(({ number: a }, { number: b }) => b - a) - .map((distribution) => ( - - ))} - - ) -} - -interface DistributionInfoProps { - distribution: UseDistributionsResultData[number] -} - -const DistributionInfo = ({ distribution }: DistributionInfoProps) => { - const { - data: addresses, - isLoading: isLoadingChainAddresses, - error: chainAddressesError, - } = useChainAddresses() - const shareAmount = distribution.distribution_shares?.[0]?.amount - const verificationSummary = distribution.distribution_verifications_summary?.[0] - const { - data: snapshotBalance, - isLoading: isLoadingSnapshotBalance, - error: snapshotBalanceError, - } = useSendBalanceOfAt({ - address: addresses?.[0]?.address, - snapshot: distribution.snapshot_id ? BigInt(distribution.snapshot_id) : undefined, - }) - const { - data: sendBalanceData, - isLoading: isLoadingSendBalance, - error: sendBalanceError, - } = useSendBalance(addresses?.[0]?.address) - const { - data: sells, - error: sendSellsError, - isLoading: isSendSellsLoading, - } = useSendSellCountDuringDistribution(distribution) - - const { - days: qualificationStartDaysRemaining, - hours: qualificationStartHoursRemaining, - minutes: qualificationStartMinutesRemaining, - seconds: qualificationStartSecondsRemaining, - diffInMs: qualificationStartDiffInMs, - } = useTimeRemaining(distribution.qualification_start) - - const { - days: qualificationEndDaysRemaining, - hours: qualificationEndHoursRemaining, - minutes: qualificationEndMinutesRemaining, - seconds: qualificationEndSecondsRemaining, - diffInMs: qualificationEndDiffInMs, - } = useTimeRemaining(distribution.qualification_end) - - const { - days: claimDaysRemaining, - hours: claimHoursRemaining, - minutes: claimMinutesRemaining, - seconds: claimSecondsRemaining, - diffInMs: claimDiffInMs, - } = useTimeRemaining(distribution.claim_end) - - return ( - - {/* Header */} - - -
- Distribution #{distribution.number} -
- {qualificationStartDiffInMs > 0 ? ( - - -
- Qualifications start in {qualificationStartDaysRemaining}d{' '} - {qualificationStartHoursRemaining}h {qualificationStartMinutesRemaining}m{' '} - {qualificationStartSecondsRemaining}s -
-
-
- ) : null} - {qualificationEndDiffInMs > 0 ? ( - - -
- Qualifications end in {qualificationEndDaysRemaining}d{' '} - {qualificationEndHoursRemaining}h {qualificationEndMinutesRemaining}m{' '} - {qualificationEndSecondsRemaining}s -
-
-
- ) : null} - {qualificationEndDiffInMs === 0 && claimDiffInMs > 0 ? ( - - -
- Claims end in {claimDaysRemaining}d {claimHoursRemaining}h {claimMinutesRemaining} - m {claimSecondsRemaining}s -
-
-
- ) : null} -
- -
- - - -

{qualificationEndDiffInMs > 0 ? 'Potential ' : ''}Rewards

-
- - -

{formatAmount(shareAmount || 0)} send

-
-
-
- - - - - - - {({ open }: { open: boolean }) => ( - <> - - - - - )} - - - - - {distribution.snapshot_id === null ? ( - - - Current Balance - - - - {isLoadingChainAddresses || isLoadingSendBalance ? ( - - ) : chainAddressesError !== null || sendBalanceError !== null ? ( - - Error:{' '} - {chainAddressesError - ? chainAddressesError.message - : sendBalanceError?.message} - - ) : ( - - {formatAmount( - formatUnits( - sendBalanceData?.value ?? BigInt(0), - sendBalanceData?.decimals ?? 0 - ) - )}{' '} - send - - )} - {Number(sendBalanceData?.value ?? BigInt(0)) < - distribution.hodler_min_balance ? ( - - Your balance is below the minimum required to qualify for rewards.{' '} - {formatAmount(distribution.hodler_min_balance, 9, 0)} send required. - 😿 - - ) : null} - - - - ) : null} - {distribution.snapshot_id !== null ? ( - - - Snapshot Balance - - - - {isLoadingChainAddresses || isLoadingSnapshotBalance ? ( - - ) : chainAddressesError !== null || snapshotBalanceError !== null ? ( - - Error:{' '} - {chainAddressesError - ? chainAddressesError.message - : snapshotBalanceError?.message} - - ) : ( - - {formatAmount(Number(snapshotBalance || 0), 9)} send - - )} - {Number(snapshotBalance || 0) < distribution.hodler_min_balance ? ( - - Your balance is below the minimum required to qualify for rewards.{' '} - {formatAmount(distribution.hodler_min_balance, 9, 0)} send required. - 😿 - - ) : null} - - - - ) : null} - - - Sells - - - - {isSendSellsLoading ? ( - - ) : sendSellsError ? ( - Error: {(sendSellsError as Error).message} - ) : sells && sells.length > 0 ? ( - `${sells.length} made during distribution. Ineligible for rewards. 😿 Use a different wallet next time.` - ) : ( - '0 made during distribution. 😺' - )} - - - {sells?.map(({ tx_hash }) => ( - - {shorten(tx_hash, 8, 3)} - - ))} - - - - - - Referrals - - - - {formatAmount(verificationSummary?.tag_referrals || 0, 5, 0)} - - - - - - Tags Registered - - - - {formatAmount(verificationSummary?.tag_registrations || 0, 5, 0)} - - - - - - Qualification Start - - - - {new Date(distribution.qualification_start).toLocaleDateString()} - - - - - - Qualification End - - - - {new Date(distribution.qualification_end).toLocaleDateString()} - - - - - - Claim End - - - - {new Date(distribution.claim_end).toLocaleDateString()} - - - - - - Total Pool - - - - {formatAmount(distribution.amount, 10, 0)} - - - - - - Minimum Balance Required - - - - - {formatAmount(distribution.hodler_min_balance, 10, 0)} send - - - - - - - - - -
-
- ) -} diff --git a/packages/app/features/distributions/components/distribution-stat-cards.tsx b/packages/app/features/distributions/components/distribution-stat-cards.tsx deleted file mode 100644 index baa32c2f3..000000000 --- a/packages/app/features/distributions/components/distribution-stat-cards.tsx +++ /dev/null @@ -1,399 +0,0 @@ -import { Card, CardProps, H6, Paragraph, Progress, Spinner, Text, XStack, YStack } from '@my/ui' -import { ServerCrash } from '@tamagui/lucide-icons' -import { IconSendToken } from 'app/components/icons/IconSendToken' -import { useActiveDistribution } from 'app/utils/distributions' -import { - DISTRIBUTION_INITIAL_POOL_AMOUNT, - useSendDistributionCurrentPoolTotal, -} from 'app/utils/distributions' -import { useTimeRemaining } from 'app/utils/useTimeRemaining' -import React, { useEffect, useState } from 'react' -import { DistributionClaimButton } from './DistributionClaimButton' - -export function DistributionsStatCard(cardProps: CardProps) { - return ( - - {cardProps.children} - - ) -} - -export function DistributionProgressCard(props: CardProps) { - const { data: sendTotalDistPool, isError, error } = useSendDistributionCurrentPoolTotal() - const [distributed, setDistributed] = useState(DISTRIBUTION_INITIAL_POOL_AMOUNT.toLocaleString()) - const [distributionProgress, setDistributionProgress] = useState(0) - useEffect(() => { - if (!sendTotalDistPool) return - const _distributed = DISTRIBUTION_INITIAL_POOL_AMOUNT - sendTotalDistPool.value - setDistributed(_distributed.toLocaleString()) - setDistributionProgress( - Number((BigInt(_distributed) * BigInt(100)) / DISTRIBUTION_INITIAL_POOL_AMOUNT) - ) - }, [sendTotalDistPool]) - - return ( - - - - - - Distributed - - - -
- {distributed} -
-
-
- - - Total for Send holders - - - - - -
- {DISTRIBUTION_INITIAL_POOL_AMOUNT.toLocaleString()} -
-
-
-
-
- - - - - {isError ? ( -
- Error: {error?.message} -
- ) : null} -
-
- ) -} - -export function DistributionTimeCard(props: CardProps) { - const { distribution, isLoading, error } = useActiveDistribution() - - const now = new Date() - const hasDistribution = !isLoading && distribution - - const isBeforeQualification = hasDistribution && now < distribution.qualification_start - const isDuringQualification = - hasDistribution && - now >= distribution.qualification_start && - now <= distribution.qualification_end - const isClaimable = - hasDistribution && now > distribution.qualification_end && now <= distribution.claim_end - const isPastClaimEnd = hasDistribution && now > distribution.claim_end - - const timeRemaining = useTimeRemaining( - isLoading - ? now - : isBeforeQualification - ? distribution?.qualification_start - : isDuringQualification - ? distribution?.qualification_end - : isClaimable - ? distribution?.claim_end - : now - ? distribution?.qualification_start - : isDuringQualification - ? distribution?.qualification_end - : isClaimable - ? distribution?.claim_end - : now - ) - - if (isLoading) { - return ( - - - - - - ) - } - - if (error) { - return ( - - - - Something went wrong. Please come back later. - - - ) - } - - if (!hasDistribution) { - return ( - - - No Active Distribution - - - ) - } - - let subHeader = '' - let dateToShow = new Date() - - if (isBeforeQualification) { - subHeader = 'Opens In' - dateToShow = distribution.qualification_start - } else if (isDuringQualification) { - subHeader = 'Closes In' - dateToShow = distribution.qualification_end - } else if (isClaimable) { - subHeader = 'Claim Ends In' - dateToShow = distribution.claim_end - } else if (isPastClaimEnd) { - subHeader = 'Claim Period Ended' - } - - return ( - - - {hasDistribution ? ( - - {subHeader} - - ) : ( - - No Active Distribution - - )} - - - - - {/* days */} - - - - {String(timeRemaining.days).padStart(2, '0')} - - - days - - - - - : - - {/* hours */} - - - - {String(timeRemaining.hours).padStart(2, '0')} - - - hrs - - - - - : - - {/* mins */} - - - - {String(timeRemaining.minutes).padStart(2, '0')} - - - min - - - - - : - - {/* secs */} - - - - {String(timeRemaining.seconds).padStart(2, '0')} - - - sec - - - - - - - - - - - ) -} diff --git a/packages/app/features/distributions/screen.tsx b/packages/app/features/distributions/screen.tsx deleted file mode 100644 index 4785ebe46..000000000 --- a/packages/app/features/distributions/screen.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { Button, Container, ScrollView, Theme, XStack, YStack } from '@my/ui' -import { sendAirdropsSafeAddress } from '@my/wagmi' -import { ArrowRight } from '@tamagui/lucide-icons' -import React from 'react' -import { useLink } from 'solito/link' -import { DistributionsTable } from './components/DistributionsTable' -import { - DistributionProgressCard, - DistributionTimeCard, -} from './components/distribution-stat-cards' - -export function DistributionsScreen() { - return ( - - - - - - - - - - ) -} - -const DistributionSection = () => { - const etherscanLink = useLink({ - href: `https://etherscan.io/address/${sendAirdropsSafeAddress}`, - }) - - return ( - - - - - - - - - - - - - - - - - ) -} diff --git a/packages/app/features/home/TokenDetails.tsx b/packages/app/features/home/TokenDetails.tsx new file mode 100644 index 000000000..cd00bc63e --- /dev/null +++ b/packages/app/features/home/TokenDetails.tsx @@ -0,0 +1,135 @@ +import { + Paragraph, + Spinner, + Tooltip, + type TooltipProps, + XStack, + type XStackProps, + useToastController, +} from '@my/ui' +import { useThemeSetting } from '@tamagui/next-theme' +import { baseMainnet } from '@my/wagmi' +import { IconArrowRight, IconError } from 'app/components/icons' +import formatAmount from 'app/utils/formatAmount' +import { type UseBalanceReturnType, useBalance } from 'wagmi' +// import { useSendAccounts } from 'app/utils/send-accounts' +import { useChainAddresses } from 'app/utils/useChainAddresses' + +const TokenDetails = ({ + coin, + ...props +}: { + coin: { label: string; token: `0x${string}` | undefined; icon: JSX.Element } +} & XStackProps) => { + // const { data: sendAccounts } = useSendAccounts() + // const sendAccount = sendAccounts?.[0] + const { data: chainAddresses } = useChainAddresses() + const sendAccount = chainAddresses?.[0] + + const balance = useBalance({ + address: sendAccount?.address, + token: coin.token, + query: { enabled: !!sendAccount }, + chainId: baseMainnet.id, + }) + + return ( + + + {coin.icon} + + {coin.label} + + + + + + + ) +} + +const TokenBalance = ({ balance }: { balance: UseBalanceReturnType }) => { + const toast = useToastController() + + const { resolvedTheme } = useThemeSetting() + const iconColor = resolvedTheme?.startsWith('dark') ? '$primary' : '$black' + + if (balance) { + if (balance.isError) { + return ( + <> + + -- + + }> + Error occurred while fetching balance. {balance.error.message} + + + ) + } + if (balance.isPending) { + return + } + if (balance?.data?.value === undefined) { + return <> + } + return ( + <> + + {formatAmount( + (Number(balance.data.value) / 10 ** (balance.data?.decimals ?? 0)).toString() + )} + + + { + // @todo go to balance details + toast.show('Coming Soon: Balance details') + }} + > + + + + ) + } +} + +const ErrorTooltip = ({ Icon, children, ...props }: TooltipProps & { Icon?: JSX.Element }) => { + return ( + + {Icon} + + + {children} + + + + ) +} + +export default TokenDetails diff --git a/packages/app/features/home/__snapshots__/screen.test.tsx.snap b/packages/app/features/home/__snapshots__/screen.test.tsx.snap index 8e22ba787..559c219b1 100644 --- a/packages/app/features/home/__snapshots__/screen.test.tsx.snap +++ b/packages/app/features/home/__snapshots__/screen.test.tsx.snap @@ -4,3123 +4,995 @@ exports[`HomeScreen 1`] = ` - - Money - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Total Balance - - - - $ - - - 6,990 - - - .00 - - - - - - - - - - - - - - - - - - - - - - Deposit + Total Balance - - - - - - - - - - - - Recieve + 0 - - - - - - - - - - - - - - Send + USD + + + + + + + + Deposit + - - - Ethereum - - - - - Cards - - - - - - - TRANSACTIONS - - - See All - - - - - - - + - - - + - - - ethantree - - - - - - - - - - - - 200 USDT ($199.98) - - - - - 0 sec ago - - - - - - + + + + + + + + + + + - - - - + - - + + + + - - - - You - - - - - - - - - - - - 1 ETH ($1,985.56) - - - - - 0 sec ago - - - - - - + + + + + + USDC + + + + + + + + + + + + - + - - - - - + - - - - You - - - - - - - - - - - - 1 ETH ($1,985.56) - - - + - - 0 sec ago - - - - - - - - + - + - + - - + + + - - - - You - - - - - - - - - - - - 1 ETH ($1,985.56) - - - - - 0 sec ago - - - - - + propList={ + [ + "fill", + ] + } + width="14.3158" + x="0" + y="0" + /> + + + + + + Ethereum + + + + + - - - - - - - - - + - - - + - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - + + + - - - - - - - - - - - - - + width="32" + x="0" + y="0" + /> + + + + + + Send + + + + + diff --git a/packages/app/features/home/layout.web.tsx b/packages/app/features/home/layout.web.tsx index b8c31e6bd..93623a418 100644 --- a/packages/app/features/home/layout.web.tsx +++ b/packages/app/features/home/layout.web.tsx @@ -1,23 +1,21 @@ -import { Container, ScrollView, YStack } from '@my/ui' -import { HomeHeader } from 'app/components/HomeHeader' +import { YStack, ScrollView } from '@my/ui' import { HomeSideBarWrapper } from 'app/components/sidebar/HomeSideBar' export function HomeLayout({ children, - header = '', -}: { children: React.ReactNode; header?: string }) { + TopNav, +}: { + children: React.ReactNode + TopNav?: React.ReactNode +}) { return ( - - - - - {header} - - - {children} - - + + + {TopNav} + + {children} + ) } diff --git a/packages/app/features/home/screen.test.tsx b/packages/app/features/home/screen.test.tsx index 693c5bc1a..9d429ec90 100644 --- a/packages/app/features/home/screen.test.tsx +++ b/packages/app/features/home/screen.test.tsx @@ -17,6 +17,18 @@ jest.mock('app/routers/params', () => ({ })) jest.mock('wagmi', () => ({ + createConfig: jest.fn(), + useChainId: jest.fn().mockReturnValue(1337), + useBalance: jest.fn().mockReturnValue({ + data: { + decimals: 6, + formatted: '0', + symbol: 'send', + value: 0n, + }, + isPending: true, + refetch: jest.fn(), + }), useAccount: jest.fn().mockReturnValue({ address: '0x123', isConnected: false, @@ -29,10 +41,6 @@ jest.mock('wagmi', () => ({ }), })) -jest.mock('app/utils/getReferralLink', () => ({ - getReferralHref: jest.fn().mockReturnValue('https://send.it/123'), -})) - jest.mock('solito/link', () => ({ Link: jest.fn(), })) @@ -41,6 +49,37 @@ jest.mock('app/utils/useUserReferralsCount', () => ({ useUserReferralsCount: jest.fn().mockReturnValue(123), })) +jest.mock('app/utils/useSendAccountBalances', () => ({ + useSendAccountBalances: jest.fn().mockReturnValue({ + balances: { + '0x3f14920c99BEB920Afa163031c4e47a3e03B3e4A': {}, + '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913': {}, + }, + totalBalance: () => 0, + }), +})) + +jest.mock('@tamagui/tooltip', () => ({ + ...jest.requireActual('@tamagui/tooltip'), + TooltipGroup: ({ children }: { children: React.ReactNode }) => <>{children}, +})) + +jest.mock('app/utils/useChainAddresses', () => ({ + useChainAddresses: jest.fn().mockReturnValue({ + data: [ + { + address: '0x123', + chainId: 1337, + }, + ], + }), +})) + +// jest.mock('@vonovak/react-native-theme-control', () => ({ +// useThemePreference: jest.fn().mockReturnValue('light'), +// setThemePreference: jest.fn(), +// })) + test('HomeScreen', () => { const tree = render( diff --git a/packages/app/features/home/screen.tsx b/packages/app/features/home/screen.tsx index 7bb982a66..369c9fb3f 100644 --- a/packages/app/features/home/screen.tsx +++ b/packages/app/features/home/screen.tsx @@ -1,363 +1,162 @@ import { - Anchor, - Avatar, Button, - Card, Container, - ListItem, Paragraph, - ScrollView, - Theme, + Spinner, + Tooltip, + TooltipGroup, XStack, YStack, + useToastController, + Stack, } from '@my/ui' import { useThemeSetting } from '@tamagui/next-theme' +import { IconDeposit, IconEthereum, IconSend, IconUSDC } from 'app/components/icons' import { - IconArrowDown, - IconClose, - IconDeposit, - IconEthereum, - IconReceive, - IconSend, - IconSendTile, - IconUSDC, -} from 'app/components/icons' -import { MainLayout } from 'app/components/layout' -import { CommentsTime } from 'app/utils/dateHelper' -import { useState } from 'react' -import { Square } from 'tamagui' + baseMainnet, + usdcAddress as usdcAddresses, + sendTokenAddress as sendAddresses, +} from '@my/wagmi' +import { useSendAccountBalances } from 'app/utils/useSendAccountBalances' +import formatAmount from 'app/utils/formatAmount' +import TokenDetails from './TokenDetails' + export function HomeScreen() { - const { resolvedTheme } = useThemeSetting() + const { totalBalance } = useSendAccountBalances() + + const toast = useToastController() const USDollar = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', }) - const [expandBalance, setExpandBalance] = useState(false) - const actionButtons = [ - { label: 'Deposit', iconPNGPath: , href: '/' }, - { label: 'Recieve', iconPNGPath: , href: '/receive' }, - { label: 'Send', iconPNGPath: , href: '/send' }, - ] - const balanceViewButtons = [ - { label: 'Ethereum', onPress: () => {} }, - { label: 'Cards', onPress: () => {} }, - ] - const transactions = [ - { - id: 1, - user: { - sendTag: 'ethantree', - }, - type: 'inbound', - amount: 200, - currency: 'USDT', - amountInUSD: 199.98, - created_on: '', - }, - { - id: 2, - user: { - sendTag: 'You', - }, - type: 'outbound', - amount: 1, - currency: 'ETH', - amountInUSD: 1985.56, - created_on: '', - }, - { - id: 3, - user: { - sendTag: 'You', - }, - type: 'outbound', - amount: 1, - currency: 'ETH', - amountInUSD: 1985.56, - created_on: '', - }, - { - id: 4, - user: { - sendTag: 'You', - }, - type: 'outbound', - amount: 1, - currency: 'ETH', - amountInUSD: 1985.56, - created_on: '', - }, + const coins = [ + { label: 'USDC', token: usdcAddresses[baseMainnet.id], icon: }, + { label: 'Ethereum', token: undefined, icon: }, + { label: 'Send', token: sendAddresses[baseMainnet.id], icon: }, ] - const balanceDetails = [ - { - currency: 'Ethereum', - symbol: 'eth', - balance: 1.45, - }, - { - currency: 'USDC', - symbol: 'usdc', - balance: 125, - }, - { - currency: 'SEND', - symbol: 'send', - balance: 71454457, - }, - { - currency: 'SEND', - symbol: 'send', - balance: 4412, - }, - { - currency: 'USDC', - symbol: 'usdc', - balance: 2.0, - }, - ] - const navigateToScreen = (href: string) => { - window.location.href = href - } + + const { resolvedTheme } = useThemeSetting() + const separatorColor = resolvedTheme?.startsWith('dark') ? '#343434' : '#E6E6E6' + return ( - <> - - + + {/* Balance Card */} + - {/* Balance Card */} - - { - !expandBalance && setExpandBalance(!expandBalance) - }} - > - - - {!expandBalance && } - - - - Total Balance - - - - {'$'} - - - {USDollar.format(6990).replace('$', '').split('.')[0]} - - - {'.00'} - - - - - - - - {expandBalance && ( - - - - - {balanceDetails.map((balance) => ( - - + + + + + + + + - - {balance.currency === 'Ethereum' ? ( - - ) : balance.currency === 'USDC' ? ( - - ) : ( - - )} - - {`${balance.currency}`} - - + Total Balance + + + + {totalBalance === undefined ? ( + + ) : ( {`${balance.balance}`} - - - ))} - - - - {/* */} - - - setExpandBalance(false)}> - + color={'$color12'} + fontFamily={'$mono'} + fontSize={'$15'} + lineHeight={'$14'} + fontWeight={'500'} + zIndex={1} + > + {formatAmount(totalBalance, 4, 0)} + + )} + + {'USD'} + + + + + + + {totalBalance === undefined ? null : USDollar.format(Number(totalBalance))} + + + + - )} - {/* D-R-S Buttons */} - - {actionButtons.map((actionButton) => ( - - navigateToScreen(actionButton.href)} - > - - {actionButton.iconPNGPath} - - - - {actionButton.label} - - ))} - - {/* Etheruem-Cards Buttons */} - - {balanceViewButtons.map((balanceViewButton) => { - return ( - - ) - })} - {/* Transactions */} - - - {'TRANSACTIONS'} - See All - - {transactions.map((transaction) => ( - + + + + + {coins.map((coin, index) => ( + + ))} - - + + ) } diff --git a/packages/app/features/leaderboard/screen.tsx b/packages/app/features/leaderboard/screen.tsx index 999f2a34e..afab95336 100644 --- a/packages/app/features/leaderboard/screen.tsx +++ b/packages/app/features/leaderboard/screen.tsx @@ -1,4 +1,4 @@ -import { Button, Container, KVTable, Paragraph, XStack, YStack } from '@my/ui' +import { Container, Paragraph, XStack, YStack } from '@my/ui' const users = [ { @@ -107,7 +107,7 @@ function LeaderBoardHeader() { Rank - Send Tag + Sendtag Points diff --git a/packages/app/features/onboarding/__snapshots__/screen.test.tsx.snap b/packages/app/features/onboarding/__snapshots__/screen.test.tsx.snap deleted file mode 100644 index 37de39a83..000000000 --- a/packages/app/features/onboarding/__snapshots__/screen.test.tsx.snap +++ /dev/null @@ -1,566 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`OnboardingScreen 1`] = ` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Congratulations on opening your first Send Account! - - - - Let's get you sending - - - - - First, fund your account by sending ETH here - - - - $ - - - - - - - - - - - -`; diff --git a/packages/app/features/onboarding/screen.tsx b/packages/app/features/onboarding/screen.tsx deleted file mode 100644 index ce372ea02..000000000 --- a/packages/app/features/onboarding/screen.tsx +++ /dev/null @@ -1,313 +0,0 @@ -/** - * Onboarding screen will ultimately be the first screen a user sees when they open the app or after they sign up. - * - * It needs to: - * - Introduce to Send - * - Create a passkey - * - Generate a deterministic address from the public key - * - Ask the user to deposit funds - */ -import { createPasskey } from '@daimo/expo-passkeys' -import { - Button, - Footer, - H2, - Input, - Label, - Paragraph, - Stack, - YStack, - Link, - XStack, - CornerTriangle, - Image, - Theme, - useMedia, - Anchor, - H3, - useToastController, -} from '@my/ui' -import { base16, base64 } from '@scure/base' -import { assert } from 'app/utils/assert' -import { base64ToBase16 } from 'app/utils/base64ToBase16' -import { COSEECDHAtoXY, parseCreateResponse } from 'app/utils/passkeys' -import { useSupabase } from 'app/utils/supabase/useSupabase' -import { useSendAccounts } from 'app/utils/send-accounts' -import { useUser } from 'app/utils/useUser' -import { daimoAccountFactory, encodeCreateAccountData, entrypoint } from 'app/utils/userop' -import { baseMainnetClient, usdcAddress } from '@my/wagmi' -import * as Device from 'expo-device' -import { concat, parseEther } from 'viem' -import { - IconSLogo, - IconSendLogo, - IconTelegramLogo, - IconXLogo, - IconCopy, -} from 'app/components/icons' -import { telegram as telegramSocial, twitter as twitterSocial } from 'app/data/socialLinks' -import { useState } from 'react' -import { getSenderAddress } from 'permissionless' -import { shorten } from 'app/utils/strings' -import { testClient } from 'app/utils/userop' -import { setERC20Balance } from 'app/utils/useSetErc20Balance' - -export function OnboardingScreen() { - const media = useMedia() - const { - data: sendAccts, - // error: sendAcctsError, - // isLoading: sendAcctsIsLoading, - } = useSendAccounts() - - return ( - - - - - - - - - - - - - - - - - - {sendAccts?.length === 0 ? ( - <> - - -

Setup Passkey

-
- - Start by creating a Passkey below. Send uses passkeys to secure your account. - -
- - - ) : ( - - )} -
- {media.gtMd && ( -
- - Connect With Us - -
- )} -
- ) -} - -/** - * Create a send account but not onchain, yet. - */ -function CreateSendAccount() { - // REMOTE / SUPABASE STATE - const supabase = useSupabase() - const { user } = useUser() - const { refetch: sendAcctsRefetch } = useSendAccounts() - const media = useMedia() - - // PASSKEY / ACCOUNT CREATION STATE - const deviceName = Device.deviceName - ? Device.deviceName - : `My ${Device.modelName ?? 'Send Account'}` - const [accountName, setAccountName] = useState(deviceName) // TODO: use expo-device to get device name - - // TODO: split creating the on-device and remote creation to introduce retries in-case of failures - async function createAccount() { - assert(!!user?.id, 'No user id') - - const keySlot = 0 - const passkeyName = `${user.id}.${keySlot}` // 64 bytes max - const [rawCred, authData] = await createPasskey({ - domain: window.location.hostname, - challengeB64: base64.encode(Buffer.from('foobar')), // TODO: generate a random challenge from the server - passkeyName, - passkeyDisplayTitle: `Send App: ${accountName}`, - }).then((r) => [r, parseCreateResponse(r)] as const) - - // store the init code in the database to avoid having to recompute it in case user drops off - // and does not finish onboarding flow - const _publicKey = COSEECDHAtoXY(authData.COSEPublicKey) - const factory = daimoAccountFactory.address - const factoryData = encodeCreateAccountData(_publicKey) - const initCode = concat([factory, factoryData]) - const senderAddress = await getSenderAddress(baseMainnetClient, { - factory, - factoryData, - entryPoint: entrypoint.address, - }) - const { error } = await supabase.rpc('create_send_account', { - send_account: { - address: senderAddress, - chain_id: baseMainnetClient.chain.id, - init_code: `\\x${initCode.slice(2)}`, - }, - webauthn_credential: { - name: passkeyName, - display_name: accountName, - raw_credential_id: `\\x${base64ToBase16(rawCred.credentialIDB64)}`, - public_key: `\\x${base16.encode(authData.COSEPublicKey)}`, - sign_count: 0, - attestation_object: `\\x${base64ToBase16(rawCred.rawAttestationObjectB64)}`, - key_type: 'ES256', - }, - key_slot: keySlot, - }) - - if (error) { - throw error - } - - if (__DEV__) { - console.log('Funding sending address', senderAddress) - await testClient.setBalance({ - address: senderAddress, - value: parseEther('1'), - }) - await setERC20Balance({ - client: testClient, - address: senderAddress, - tokenAddress: usdcAddress[baseMainnetClient.chain.id], - value: BigInt(100e6), - }) - } - - await sendAcctsRefetch() - - // await createAccountUsingEOA(_publicKey) - } - - return ( - // TODO: turn into a form - - - - {media.gtMd ? ( - - - Why Passkey? - - - - - - ) : ( - - - - )} - - ) -} - -function SendAccountCongratulations() { - const toast = useToastController() - const { data: sendAccts } = useSendAccounts() - const sendAcct = sendAccts?.[0] - - return ( - -

Congratulations on opening your first Send Account!

- Let's get you sending - - First, fund your account by sending ETH here - - - ${shorten(sendAcct?.address)} - - - - - {__DEV__ && !!sendAcct && ( - - - - ⭐️ Secret Shop ⭐️ - Available on Localnet/Testnet only. - - - - - - )} - -
- ) -} diff --git a/packages/app/features/profile/AvatarProfile.tsx b/packages/app/features/profile/AvatarProfile.tsx index 52fa09ef4..716b232e1 100644 --- a/packages/app/features/profile/AvatarProfile.tsx +++ b/packages/app/features/profile/AvatarProfile.tsx @@ -1,5 +1,5 @@ -import { Avatar, AvatarProps, SizableText } from '@my/ui' -import { ProfileProp } from './screen' +import { Avatar, type AvatarProps, SizableText } from '@my/ui' +import type { ProfileProp } from './screen' export function AvatarProfile({ profile, ...rest }: AvatarProps & { profile: ProfileProp }) { return ( diff --git a/packages/app/features/profile/SendDialog.test.tsx b/packages/app/features/profile/SendDialog.test.tsx index 390870cf1..6f3c9c7a3 100644 --- a/packages/app/features/profile/SendDialog.test.tsx +++ b/packages/app/features/profile/SendDialog.test.tsx @@ -1,4 +1,4 @@ -import { describe, test } from '@jest/globals' +import { jest, describe, test, beforeEach, expect } from '@jest/globals' import { act, render, userEvent, screen, waitFor } from '@testing-library/react-native' import { Wrapper } from 'app/utils/__mocks__/Wrapper' import { SendDialog } from './SendDialog' @@ -42,7 +42,7 @@ describe('SendDialog', () => { return View }, }) - const mockMutateAsync = jest.fn().mockResolvedValue({ + const mockMutateAsync = jest.fn().mockReturnValueOnce({ hash: '0x123', success: true, receipt: { @@ -74,6 +74,8 @@ describe('SendDialog', () => { await act(async () => { jest.runAllTimers() + jest.advanceTimersByTime(2000) + jest.runAllTimers() }) const user = userEvent.setup() @@ -90,9 +92,12 @@ describe('SendDialog', () => { expect(submit).toBeOnTheScreen() await act(async () => { await submit.props.onPress() // trigger validation + jest.advanceTimersByTime(2000) jest.runAllTimers() }) - expect(screen.getByText('Required')).toBeOnTheScreen() + // @todo figure out how to get errors to show with tooltips + // await waitFor(() => screen.getByText('Required')) + // expect(screen.getByText('Required')).toBeOnTheScreen() expect(screen.toJSON()).toMatchSnapshot('SendForm Error') await act(async () => { await user.type(amount, '3.50') diff --git a/packages/app/features/profile/SendDialog.tsx b/packages/app/features/profile/SendDialog.tsx index dfa681a6a..fa8457e5b 100644 --- a/packages/app/features/profile/SendDialog.tsx +++ b/packages/app/features/profile/SendDialog.tsx @@ -3,7 +3,7 @@ import { Button, Container, Dialog, - DialogProps, + type DialogProps, Sheet, SizableText, XStack, @@ -12,7 +12,7 @@ import { } from '@my/ui' import { IconClose } from 'app/components/icons' import { AvatarProfile } from './AvatarProfile' -import { useProfileLookup } from 'app/utils/useProfileLookup' +import type { useProfileLookup } from 'app/utils/useProfileLookup' import { Provider } from 'app/provider' import { SendForm } from './SendForm' diff --git a/packages/app/features/profile/SendForm.tsx b/packages/app/features/profile/SendForm.tsx index 713bfc15b..6e56389d5 100644 --- a/packages/app/features/profile/SendForm.tsx +++ b/packages/app/features/profile/SendForm.tsx @@ -3,10 +3,10 @@ import { z } from 'zod' import { SchemaForm, formFields } from 'app/utils/SchemaForm' import { FormProvider, useForm } from 'react-hook-form' import { useSendAccounts } from 'app/utils/send-accounts' -import { Hex, formatUnits, parseUnits, isAddress, isHex } from 'viem' -import { baseMainnet, usdcAddress as usdcAddresses } from '@my/wagmi' +import { type Hex, formatUnits, parseUnits, isAddress, isHex } from 'viem' +import { baseMainnet, sendTokenAddress, usdcAddress as usdcAddresses } from '@my/wagmi' import { useState } from 'react' -import { ProfileProp } from './SendDialog' +import type { ProfileProp } from './SendDialog' import { useBalance, useTransactionCount } from 'wagmi' import formatAmount from 'app/utils/formatAmount' import { useSendAccountInitCode } from 'app/utils/useSendAccountInitCode' @@ -105,6 +105,7 @@ export function SendForm({ profile }: { profile: ProfileProp }) { options: [ { name: 'ETH', value: '' }, { name: 'USDC', value: usdcAddresses[baseMainnet.id] }, + { name: 'SEND', value: sendTokenAddress[baseMainnet.id] }, ], }, }} diff --git a/packages/app/features/profile/__snapshots__/SendDialog.test.tsx.snap b/packages/app/features/profile/__snapshots__/SendDialog.test.tsx.snap index ac9357700..b27946310 100644 --- a/packages/app/features/profile/__snapshots__/SendDialog.test.tsx.snap +++ b/packages/app/features/profile/__snapshots__/SendDialog.test.tsx.snap @@ -20,12 +20,159 @@ exports[`SendDialog it can send: SendForm 1`] = ` animatedStyle={ { "value": { + "backgroundColor": { + "A": { + "current": 0.9019607843137255, + "lastTimestamp": 0, + "omega0": 7.745966692414834, + "omega1": NaN, + "onFrame": [Function], + "onStart": [Function], + "reduceMotion": undefined, + "startTimestamp": 0, + "startValue": 0.9019607843137255, + "toValue": 0.9019607843137255, + "velocity": 0, + "zeta": 1.2909944487358056, + }, + "B": { + "current": 0, + "lastTimestamp": 0, + "omega0": 7.745966692414834, + "omega1": NaN, + "onFrame": [Function], + "onStart": [Function], + "reduceMotion": undefined, + "startTimestamp": 0, + "startValue": 0, + "toValue": 0, + "velocity": 0, + "zeta": 1.2909944487358056, + }, + "G": { + "current": 0, + "lastTimestamp": 0, + "omega0": 7.745966692414834, + "omega1": NaN, + "onFrame": [Function], + "onStart": [Function], + "reduceMotion": undefined, + "startTimestamp": 0, + "startValue": 0, + "toValue": 0, + "velocity": 0, + "zeta": 1.2909944487358056, + }, + "R": { + "current": 0, + "lastTimestamp": 0, + "omega0": 7.745966692414834, + "omega1": NaN, + "onFrame": [Function], + "onStart": [Function], + "reduceMotion": undefined, + "startTimestamp": 0, + "startValue": 0, + "toValue": 0, + "velocity": 0, + "zeta": 1.2909944487358056, + }, + "callStart": null, + "callback": [Function], + "current": "rgba(0, 0, 0, 0.9019607843137255)", + "finished": true, + "lastTimestamp": 0, + "omega0": 0, + "omega1": 0, + "onFrame": [Function], + "onStart": [Function], + "reduceMotion": false, + "startTimestamp": 0, + "startValue": 0, + "timestamp": 0, + "toValue": "rgba(0,0,0,0.9)", + "velocity": 0, + "zeta": 0, + }, + "bottom": { + "callStart": null, + "callback": [Function], + "current": 0, + "finished": true, + "lastTimestamp": 0, + "omega0": 7.745966692414834, + "omega1": NaN, + "onFrame": [Function], + "onStart": [Function], + "reduceMotion": false, + "startTimestamp": 0, + "startValue": 0, + "timestamp": 0, + "toValue": 0, + "velocity": 0, + "zeta": 1.2909944487358056, + }, + "left": { + "callStart": null, + "callback": [Function], + "current": 0, + "finished": true, + "lastTimestamp": 0, + "omega0": 7.745966692414834, + "omega1": NaN, + "onFrame": [Function], + "onStart": [Function], + "reduceMotion": false, + "startTimestamp": 0, + "startValue": 0, + "timestamp": 0, + "toValue": 0, + "velocity": 0, + "zeta": 1.2909944487358056, + }, "opacity": 0.7, + "right": { + "callStart": null, + "callback": [Function], + "current": 0, + "finished": true, + "lastTimestamp": 0, + "omega0": 7.745966692414834, + "omega1": NaN, + "onFrame": [Function], + "onStart": [Function], + "reduceMotion": false, + "startTimestamp": 0, + "startValue": 0, + "timestamp": 0, + "toValue": 0, + "velocity": 0, + "zeta": 1.2909944487358056, + }, + "top": { + "callStart": null, + "callback": [Function], + "current": 0, + "finished": true, + "lastTimestamp": 0, + "omega0": 7.745966692414834, + "omega1": NaN, + "onFrame": [Function], + "onStart": [Function], + "reduceMotion": false, + "startTimestamp": 0, + "startValue": 0, + "timestamp": 0, + "toValue": 0, + "velocity": 0, + "zeta": 1.2909944487358056, + }, }, } } cancelable={true} collapsable={false} + disabled={false} focusable={true} forwardedRef={[Function]} minPressDuration={0} @@ -63,6 +210,7 @@ exports[`SendDialog it can send: SendForm 1`] = ` } animation="medium" collapsable={false} + disableClassName={true} forwardedRef={[Function]} onLayout={[Function]} pointerEvents="auto" @@ -169,7 +317,8 @@ exports[`SendDialog it can send: SendForm 1`] = ` accessibilityLabel="Dialog Close" accessibilityRole="button" cancelable={true} - cursor="pointer" + disabled={false} + focusVisibleStyle={{}} focusable={[Function]} minPressDuration={0} onBlur={[Function]} @@ -462,10 +611,10 @@ exports[`SendDialog it can send: SendForm 1`] = ` >
+ - Send - - + suppressHighlighting={true} + userSelect="none" + > + Send + +
@@ -1054,6 +1229,7 @@ exports[`SendDialog it can send: SendForm 1`] = ` } } collapsable={false} + disableClassName={true} forwardedRef={[Function]} onLayout={[Function]} pointerEvents="none" @@ -1111,8 +1287,9 @@ exports[`SendDialog it can send: SendForm 1`] = ` } > + + + + SEND + + @@ -1357,7 +1627,7 @@ exports[`SendDialog it can send: SendForm 1`] = ` "borderTopLeftRadius": 0, "borderTopRightRadius": 0, "borderTopWidth": 0, - "bottom": "-50%", + "bottom": "-100%", "flex": 1, "flexDirection": "column", "height": 0, @@ -1390,7 +1660,7 @@ exports[`SendDialog it can send: SendForm 1`] = ` "borderTopLeftRadius": 0, "borderTopRightRadius": 0, "borderTopWidth": 0, - "bottom": "-50%", + "bottom": "-100%", "flex": 1, "flexDirection": "column", "height": 0, @@ -1434,12 +1704,159 @@ exports[`SendDialog it can send: SendForm Error 1`] = ` animatedStyle={ { "value": { + "backgroundColor": { + "A": { + "current": 0.9019607843137255, + "lastTimestamp": 0, + "omega0": 7.745966692414834, + "omega1": NaN, + "onFrame": [Function], + "onStart": [Function], + "reduceMotion": undefined, + "startTimestamp": 0, + "startValue": 0.9019607843137255, + "toValue": 0.9019607843137255, + "velocity": 0, + "zeta": 1.2909944487358056, + }, + "B": { + "current": 0, + "lastTimestamp": 0, + "omega0": 7.745966692414834, + "omega1": NaN, + "onFrame": [Function], + "onStart": [Function], + "reduceMotion": undefined, + "startTimestamp": 0, + "startValue": 0, + "toValue": 0, + "velocity": 0, + "zeta": 1.2909944487358056, + }, + "G": { + "current": 0, + "lastTimestamp": 0, + "omega0": 7.745966692414834, + "omega1": NaN, + "onFrame": [Function], + "onStart": [Function], + "reduceMotion": undefined, + "startTimestamp": 0, + "startValue": 0, + "toValue": 0, + "velocity": 0, + "zeta": 1.2909944487358056, + }, + "R": { + "current": 0, + "lastTimestamp": 0, + "omega0": 7.745966692414834, + "omega1": NaN, + "onFrame": [Function], + "onStart": [Function], + "reduceMotion": undefined, + "startTimestamp": 0, + "startValue": 0, + "toValue": 0, + "velocity": 0, + "zeta": 1.2909944487358056, + }, + "callStart": null, + "callback": [Function], + "current": "rgba(0, 0, 0, 0.9019607843137255)", + "finished": true, + "lastTimestamp": 0, + "omega0": 0, + "omega1": 0, + "onFrame": [Function], + "onStart": [Function], + "reduceMotion": false, + "startTimestamp": 0, + "startValue": 0, + "timestamp": 0, + "toValue": "rgba(0,0,0,0.9)", + "velocity": 0, + "zeta": 0, + }, + "bottom": { + "callStart": null, + "callback": [Function], + "current": 0, + "finished": true, + "lastTimestamp": 0, + "omega0": 7.745966692414834, + "omega1": NaN, + "onFrame": [Function], + "onStart": [Function], + "reduceMotion": false, + "startTimestamp": 0, + "startValue": 0, + "timestamp": 0, + "toValue": 0, + "velocity": 0, + "zeta": 1.2909944487358056, + }, + "left": { + "callStart": null, + "callback": [Function], + "current": 0, + "finished": true, + "lastTimestamp": 0, + "omega0": 7.745966692414834, + "omega1": NaN, + "onFrame": [Function], + "onStart": [Function], + "reduceMotion": false, + "startTimestamp": 0, + "startValue": 0, + "timestamp": 0, + "toValue": 0, + "velocity": 0, + "zeta": 1.2909944487358056, + }, "opacity": 0.7, + "right": { + "callStart": null, + "callback": [Function], + "current": 0, + "finished": true, + "lastTimestamp": 0, + "omega0": 7.745966692414834, + "omega1": NaN, + "onFrame": [Function], + "onStart": [Function], + "reduceMotion": false, + "startTimestamp": 0, + "startValue": 0, + "timestamp": 0, + "toValue": 0, + "velocity": 0, + "zeta": 1.2909944487358056, + }, + "top": { + "callStart": null, + "callback": [Function], + "current": 0, + "finished": true, + "lastTimestamp": 0, + "omega0": 7.745966692414834, + "omega1": NaN, + "onFrame": [Function], + "onStart": [Function], + "reduceMotion": false, + "startTimestamp": 0, + "startValue": 0, + "timestamp": 0, + "toValue": 0, + "velocity": 0, + "zeta": 1.2909944487358056, + }, }, } } cancelable={true} collapsable={false} + disabled={false} focusable={true} forwardedRef={[Function]} minPressDuration={0} @@ -1477,6 +1894,7 @@ exports[`SendDialog it can send: SendForm Error 1`] = ` } animation="medium" collapsable={false} + disableClassName={true} forwardedRef={[Function]} onLayout={[Function]} pointerEvents="auto" @@ -1583,7 +2001,8 @@ exports[`SendDialog it can send: SendForm Error 1`] = ` accessibilityLabel="Dialog Close" accessibilityRole="button" cancelable={true} - cursor="pointer" + disabled={false} + focusVisibleStyle={{}} focusable={[Function]} minPressDuration={0} onBlur={[Function]} @@ -1876,10 +2295,10 @@ exports[`SendDialog it can send: SendForm Error 1`] = ` >
- + - Required - + tintColor="#C4292F" + vbHeight={24} + vbWidth={24} + width={24} + > + + + + + + +
- Send - - + suppressHighlighting={true} + userSelect="none" + > + Send + + + + + + SEND + + @@ -2848,7 +3465,7 @@ exports[`SendDialog it can send: SendForm Error 1`] = ` "borderTopLeftRadius": 0, "borderTopRightRadius": 0, "borderTopWidth": 0, - "bottom": "-50%", + "bottom": "-100%", "flex": 1, "flexDirection": "column", "height": 0, @@ -2881,7 +3498,7 @@ exports[`SendDialog it can send: SendForm Error 1`] = ` "borderTopLeftRadius": 0, "borderTopRightRadius": 0, "borderTopWidth": 0, - "bottom": "-50%", + "bottom": "-100%", "flex": 1, "flexDirection": "column", "height": 0, diff --git a/packages/app/features/profile/__snapshots__/screen.test.tsx.snap b/packages/app/features/profile/__snapshots__/screen.test.tsx.snap index 8cf5705f1..5454eab55 100644 --- a/packages/app/features/profile/__snapshots__/screen.test.tsx.snap +++ b/packages/app/features/profile/__snapshots__/screen.test.tsx.snap @@ -198,7 +198,8 @@ exports[`ProfileScreen: ProfileScreen 1`] = ` + + + + + ) + } + + if (!sendAcct) { + return ( + + No Send Account found. Did you create one? + + ) + } + + return ( + + + + + + + + Available on {baseMainnet.name} only. Your Send Account Address: + + + + + {sendAcct.address} + + + + + {__DEV__ && baseMainnet.id !== 84532 ? ( + <> + + + + + ) : ( + + + {fundError && {fundError.message}} + {fundData && ( + + Result + {Object.entries(fundData).map(([key, value]) => + value ? ( + + + {key} transaction: {shorten(value)} + + + ) : ( + {key}: too much balance. + ) + )} + + )} + + )} + + + + + + ) +} diff --git a/packages/app/features/send/components/modal/GradientButton.tsx b/packages/app/features/send/components/modal/GradientButton.tsx index d03a13290..d8732d156 100644 --- a/packages/app/features/send/components/modal/GradientButton.tsx +++ b/packages/app/features/send/components/modal/GradientButton.tsx @@ -1,4 +1,4 @@ -import { Button, ButtonProps, Paragraph } from '@my/ui' +import { Button, type ButtonProps, Paragraph } from '@my/ui' export const GradientButton = ({ children, ...props }: ButtonProps) => { return ( diff --git a/packages/app/features/send/components/modal/profile-modal.tsx b/packages/app/features/send/components/modal/profile-modal.tsx index 52208d807..45b5a1cf9 100644 --- a/packages/app/features/send/components/modal/profile-modal.tsx +++ b/packages/app/features/send/components/modal/profile-modal.tsx @@ -6,14 +6,14 @@ import { Sheet, SizableText, Theme, - ThemeName, + type ThemeName, XStack, YStack, } from '@my/ui' import { useThemeSetting } from '@tamagui/next-theme' import { IconClose, IconQRCode, IconShare } from 'app/components/icons' import { useSubScreenContext } from 'app/features/send/providers' -import { ANIMATE_DIRECTION_RIGHT, IProfileModalProps, QRScreen } from 'app/features/send/types' +import { ANIMATE_DIRECTION_RIGHT, type IProfileModalProps, QRScreen } from 'app/features/send/types' import { useState } from 'react' import { ProfileQRModal } from './profile-qr-modal' diff --git a/packages/app/features/send/components/modal/profile-qr-modal.tsx b/packages/app/features/send/components/modal/profile-qr-modal.tsx index a20ca56fe..11d6de77e 100644 --- a/packages/app/features/send/components/modal/profile-qr-modal.tsx +++ b/packages/app/features/send/components/modal/profile-qr-modal.tsx @@ -1,7 +1,7 @@ -import { Adapt, Button, Dialog, Sheet, SizableText, Theme, ThemeName, YStack } from '@my/ui' +import { Adapt, Button, Dialog, Sheet, SizableText, Theme, type ThemeName, YStack } from '@my/ui' import { useThemeSetting } from '@tamagui/next-theme' import { IconArrowLeft, IconClose } from 'app/components/icons' -import { IProfileQRModalProps } from 'app/features/send/types' +import type { IProfileQRModalProps } from 'app/features/send/types' export const ProfileQRModal = ({ showModal, setShowModal, to }: IProfileQRModalProps) => { const { resolvedTheme } = useThemeSetting() diff --git a/packages/app/features/send/components/modal/request-confirm-modal.tsx b/packages/app/features/send/components/modal/request-confirm-modal.tsx index 55ed044f9..51892cfe5 100644 --- a/packages/app/features/send/components/modal/request-confirm-modal.tsx +++ b/packages/app/features/send/components/modal/request-confirm-modal.tsx @@ -7,7 +7,7 @@ import { Sheet, SizableText, Theme, - ThemeName, + type ThemeName, XStack, YStack, styled, @@ -15,7 +15,7 @@ import { import { useThemeSetting } from '@tamagui/next-theme' import { IconClose } from 'app/components/icons' import { useTransferContext } from 'app/features/send/providers/transfer-provider' -import { IConfirmModalProps } from 'app/features/send/types' +import type { IConfirmModalProps } from 'app/features/send/types' import { GradientButton } from './GradientButton' const CustomInput = styled(Input, { diff --git a/packages/app/features/send/components/modal/send-confirm-modal.tsx b/packages/app/features/send/components/modal/send-confirm-modal.tsx index af5537b8e..0a40f61b1 100644 --- a/packages/app/features/send/components/modal/send-confirm-modal.tsx +++ b/packages/app/features/send/components/modal/send-confirm-modal.tsx @@ -7,16 +7,15 @@ import { Sheet, SizableText, Theme, - ThemeName, + type ThemeName, XStack, YStack, styled, } from '@my/ui' import { useThemeSetting } from '@tamagui/next-theme' import { IconClose } from 'app/components/icons' -import { SendButton } from 'app/components/layout/footer/components/SendButton' import { useTransferContext } from 'app/features/send/providers/transfer-provider' -import { IConfirmModalProps } from 'app/features/send/types' +import type { IConfirmModalProps } from 'app/features/send/types' const CustomInput = styled(Input, { name: 'CustomInput', @@ -122,7 +121,7 @@ export const SendConfirmModal = ({ showModal, setShowModal }: IConfirmModalProps - + - - - - - - - - { - if (code) { - try { - // write the referral link to clipboard - // @TODO: implement a native clipboard solution - navigator.clipboard.writeText(referralHref) - } catch (e) { - console.warn(e) - prompt('Copy to clipboard: Ctrl+C, Enter', referralHref) - } - toast.show('Copied your referral link to clipboard') - } - }} - > - - - - - Referral Link - - - - - {code} - - - - - - - - - ACCOUNT & SETTINGS - - - - {accountSettings.map((account) => ( - - - - {account.icon} - - {account.label} - - - - - - - - ))} - - {accountSocialMedia.map((account) => ( - - - - {account.icon} - - {account.label} - - - - - - - - ))} - - {accountTheme.map((account) => ( - - account.action()}> - - {account.icon} - - {account.label} - - - - {account.label !== 'Theme' ? ( - - ) : ( - - {mode?.toUpperCase()} - - )} - - - - ))} - - - - - - ) -} diff --git a/packages/app/features/unknown/screen.tsx b/packages/app/features/unknown/screen.tsx index 4031e4d8e..394216b23 100644 --- a/packages/app/features/unknown/screen.tsx +++ b/packages/app/features/unknown/screen.tsx @@ -7,7 +7,7 @@ export function UnknownScreen() {

Not found.

Send, Instant Payments.

- +
diff --git a/packages/app/index.ts b/packages/app/index.ts index 066a3c922..d23b7228e 100644 --- a/packages/app/index.ts +++ b/packages/app/index.ts @@ -1,4 +1,4 @@ // leave this blank // don't re-export files from this workspace. it'll break next.js tree shaking // https://github.com/vercel/next.js/issues/12557 -export {} +export type {} diff --git a/packages/app/jest.config.ts b/packages/app/jest.config.ts index d0a35bdfc..6134e45c9 100644 --- a/packages/app/jest.config.ts +++ b/packages/app/jest.config.ts @@ -125,7 +125,7 @@ const config: Config = { // restoreMocks: false, // The root directory that Jest should scan for tests and modules within - // rootDir: undefined, + // rootDir: '.', // A list of paths to directories that Jest should use to search for files in // roots: [ @@ -164,7 +164,7 @@ const config: Config = { // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped transformIgnorePatterns: [ - 'node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|moti|sentry-expo|native-base|react-native-svg|solito|@wagmi|wagmi/*|viem|viem/*|@tamagui/animations-moti)', + 'node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|moti|sentry-expo|native-base|react-native-svg|solito|@rainbow-me/rainbowkit|@wagmi|wagmi/*|viem|viem/*|@tamagui/animations-moti)', ], moduleNameMapper: { @@ -194,7 +194,7 @@ const config: Config = { // unmockedModulePathPatterns: undefined, // Indicates whether each individual test should be reported during the run - // verbose: undefined, + verbose: true, // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode // watchPathIgnorePatterns: [], diff --git a/packages/app/jest.setup.ts b/packages/app/jest.setup.ts index 715c27eb1..4bc7ce6bd 100644 --- a/packages/app/jest.setup.ts +++ b/packages/app/jest.setup.ts @@ -5,6 +5,6 @@ nock.disableNetConnect() jest.mock('@react-navigation/native') jest.mock('@daimo/expo-passkeys') -jest.mock('app/provider') jest.mock('app/utils/send-accounts') jest.mock('@tamagui/toast') +jest.mock('react-native/Libraries/EventEmitter/NativeEventEmitter') diff --git a/packages/app/package.json b/packages/app/package.json index 258e3fae4..6a5aee2c3 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -13,46 +13,48 @@ }, "dependencies": { "@daimo/expo-passkeys": "workspace:*", - "@my/contracts": "workspace:*", "@my/supabase": "workspace:*", "@my/ui": "workspace:*", "@my/wagmi": "workspace:*", "@noble/curves": "^1.2.0", "@openzeppelin/merkle-tree": "1.0.5", - "@react-native-async-storage/async-storage": "1.18.2", + "@react-native-async-storage/async-storage": "1.21.0", "@scure/base": "^1.1.5", - "@supabase/auth-helpers-nextjs": "^0.8.7", + "@supabase/auth-helpers-nextjs": "^0.9.0", "@supabase/auth-helpers-react": "^0.4.2", - "@supabase/postgrest-js": "^1.8.6", - "@supabase/supabase-js": "^2.38.5", - "@tamagui/animations-react-native": "^1.88.8", - "@tamagui/colors": "^1.88.8", - "@tamagui/font-inter": "^1.88.8", - "@tamagui/helpers-icon": "^1.88.8", - "@tamagui/lucide-icons": "^1.88.8", - "@tamagui/shorthands": "^1.88.8", - "@tamagui/themes": "^1.88.8", + "@supabase/postgrest-js": "^1.9.2", + "@supabase/supabase-js": "^2.39.3", + "@tamagui/animations-react-native": "^1.93.2", + "@tamagui/colors": "^1.93.2", + "@tamagui/helpers-icon": "^1.93.2", + "@tamagui/lucide-icons": "^1.93.2", + "@tamagui/shorthands": "^1.93.2", + "@tamagui/themes": "^1.93.2", "@tanstack/react-query": "^5.18.1", "@trpc/client": "11.0.0-next-beta.264", "@vonovak/react-native-theme-control": "^5.0.1", "@wagmi/chains": "^1.8.0", "base64-arraybuffer": "^1.0.2", - "burnt": "^0.11.7", + "burnt": "^0.12.1", "cbor": "^9.0.1", "dnum": "^2.9.0", - "expo-constants": "~14.4.2", - "expo-device": "5.6.0", - "expo-image-picker": "~14.3.1", - "expo-linking": "~5.0.2", - "expo-secure-store": "~12.3.1", - "permissionless": "^0.1.0", + "expo-clipboard": "^5.0.1", + "expo-constants": "~15.4.5", + "expo-device": "~5.9.3", + "expo-image-picker": "~14.7.1", + "expo-linking": "~6.2.2", + "expo-secure-store": "~12.8.1", + "expo-sharing": "~11.5.0", + "libphonenumber-js": "^1.10.58", + "permissionless": "^0.1.11", "react-hook-form": "^7.48.2", - "react-native-safe-area-context": "4.6.3", - "react-native-svg": "13.9.0", + "react-native": "0.73.6", + "react-native-safe-area-context": "4.8.2", + "react-native-svg": "14.1.0", "solito": "^4.0.1", "superjson": "^1.13.1", - "viem": "^2.7.6", - "wagmi": "^2.5.5", + "viem": "^2.8.10", + "wagmi": "2.5.7", "zod": "^3.22.4" }, "devDependencies": { @@ -60,12 +62,12 @@ "@tanstack/eslint-plugin-query": "^5.20.1", "@testing-library/react-native": "^12.4.3", "@types/react": "^18.2.15", - "babel-preset-expo": "9.5.2", + "babel-preset-expo": "^10.0.1", "debug": "^4.3.4", "eslint": "^8.46.0", "eslint-config-custom": "workspace:*", "jest": "^29.7.0", - "jest-expo": "^49.0.0", + "jest-expo": "^50.0.0", "nock": "^14.0.0-beta.2", "react-test-renderer": "^18.2.0" } diff --git a/packages/app/provider/auth/AuthProvider.native.tsx b/packages/app/provider/auth/AuthProvider.native.tsx index d363117d0..69fb199b6 100644 --- a/packages/app/provider/auth/AuthProvider.native.tsx +++ b/packages/app/provider/auth/AuthProvider.native.tsx @@ -1,10 +1,10 @@ -import { Session, SessionContext as SessionContextHelper } from '@supabase/auth-helpers-react' -import { AuthError, User } from '@supabase/supabase-js' +import type { Session, SessionContext as SessionContextHelper } from '@supabase/auth-helpers-react' +import { AuthError, type User } from '@supabase/supabase-js' import { supabase } from 'app/utils/supabase/client.native' import { router, useSegments } from 'expo-router' import { createContext, useEffect, useState } from 'react' import { Platform } from 'react-native' -import { AuthProviderProps } from './AuthProvider' +import type { AuthProviderProps } from './AuthProvider' import { AuthStateChangeHandler } from './AuthStateChangeHandler' export const SessionContext = createContext({ @@ -73,7 +73,7 @@ export function useProtectedRoute(user: User | null) { !inAuthGroup ) { // Redirect to the sign-in page. - replaceRoute('/onboarding') + replaceRoute('/auth/onboarding') } else if (user && inAuthGroup) { // Redirect away from the sign-in page. replaceRoute('/') diff --git a/packages/app/provider/auth/AuthProvider.tsx b/packages/app/provider/auth/AuthProvider.tsx index c13e784dd..bbc997e6a 100644 --- a/packages/app/provider/auth/AuthProvider.tsx +++ b/packages/app/provider/auth/AuthProvider.tsx @@ -1,5 +1,5 @@ -import { Database } from '@my/supabase/database.types' -import { Session, createPagesBrowserClient } from '@supabase/auth-helpers-nextjs' +import type { Database } from '@my/supabase/database.types' +import { type Session, createPagesBrowserClient } from '@supabase/auth-helpers-nextjs' import { SessionContextProvider } from '@supabase/auth-helpers-react' import { useState } from 'react' import { AuthStateChangeHandler } from './AuthStateChangeHandler' diff --git a/packages/app/provider/auth/AuthStateChangeHandler.ts b/packages/app/provider/auth/AuthStateChangeHandler.ts index 883d07a61..a84f8edaa 100644 --- a/packages/app/provider/auth/AuthStateChangeHandler.ts +++ b/packages/app/provider/auth/AuthStateChangeHandler.ts @@ -8,7 +8,7 @@ const useRedirectAfterSignOut = () => { useEffect(() => { const signOutListener = supabase.auth.onAuthStateChange((event) => { if (event === 'SIGNED_OUT') { - router.replace('/sign-in') + router.replace('/auth/sign-in') } }) return () => { diff --git a/packages/app/provider/index.tsx b/packages/app/provider/index.tsx index 192812d08..8ecef3296 100644 --- a/packages/app/provider/index.tsx +++ b/packages/app/provider/index.tsx @@ -1,5 +1,5 @@ -import { Session } from '@supabase/supabase-js' -import React from 'react' +import type { Session } from '@supabase/supabase-js' +import type React from 'react' import { AuthProvider } from './auth' import { QueryClientProvider } from './react-query' import { SafeAreaProvider } from './safe-area' @@ -7,6 +7,7 @@ import { TamaguiProvider } from './tamagui' import { UniversalThemeProvider } from './theme' import { ToastProvider } from './toast' import { WagmiProvider } from './wagmi' +import { RainbowkitProvider } from './rainbowkit' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' @@ -41,6 +42,7 @@ const compose = (providers: React.FC<{ children: React.ReactNode }>[]) => const Providers = compose([ WagmiProvider, + RainbowkitProvider, UniversalThemeProvider, SafeAreaProvider, TamaguiProvider, diff --git a/packages/app/provider/rainbowkit/RainbowkitProvider.native.tsx b/packages/app/provider/rainbowkit/RainbowkitProvider.native.tsx new file mode 100644 index 000000000..85701bf32 --- /dev/null +++ b/packages/app/provider/rainbowkit/RainbowkitProvider.native.tsx @@ -0,0 +1,4 @@ +// noop on native for now... +export function RainbowkitProvider({ children }: { children: React.ReactNode }) { + return <>{children} +} diff --git a/packages/app/provider/rainbowkit/RainbowkitProvider.tsx b/packages/app/provider/rainbowkit/RainbowkitProvider.tsx new file mode 100644 index 000000000..f9efd55dc --- /dev/null +++ b/packages/app/provider/rainbowkit/RainbowkitProvider.tsx @@ -0,0 +1,6 @@ +import { RainbowKitProvider } from '@rainbow-me/rainbowkit' +import { baseMainnetClient } from 'app/utils/viem' + +export function RainbowkitProvider({ children }: { children: React.ReactNode }) { + return {children} +} diff --git a/packages/app/provider/rainbowkit/index.ts b/packages/app/provider/rainbowkit/index.ts new file mode 100644 index 000000000..2c5ee9993 --- /dev/null +++ b/packages/app/provider/rainbowkit/index.ts @@ -0,0 +1 @@ +export * from './RainbowkitProvider' diff --git a/packages/app/provider/tag-search/TagSearchProvider.tsx b/packages/app/provider/tag-search/TagSearchProvider.tsx index f84525e82..20783c8da 100644 --- a/packages/app/provider/tag-search/TagSearchProvider.tsx +++ b/packages/app/provider/tag-search/TagSearchProvider.tsx @@ -1,11 +1,11 @@ -import { Functions } from '@my/supabase/database.types' +import type { Functions } from '@my/supabase/database.types' import { useSupabase } from 'app/utils/supabase/useSupabase' import { createContext, useContext, useEffect, useMemo, useState } from 'react' import { z } from 'zod' import { useForm } from 'react-hook-form' import { useDebounce } from '@my/ui' import { formFields } from '../../utils/SchemaForm' -import { PostgrestError } from '@supabase/supabase-js' +import type { PostgrestError } from '@supabase/supabase-js' export const SearchSchema = z.object({ query: formFields.text, diff --git a/packages/app/provider/tamagui/TamaguiProvider.tsx b/packages/app/provider/tamagui/TamaguiProvider.tsx index ce139561e..f2a50c89e 100644 --- a/packages/app/provider/tamagui/TamaguiProvider.tsx +++ b/packages/app/provider/tamagui/TamaguiProvider.tsx @@ -2,10 +2,16 @@ import { TamaguiProvider as TamaguiProviderOG } from '@my/ui' import config from '../../tamagui.config' import { useRootTheme } from '../theme/UniversalThemeProvider' +import { useMemo } from 'react' export const TamaguiProvider = ({ children }: { children: React.ReactNode }) => { const [rootTheme] = useRootTheme() + // memo to avoid re-render on dark/light change + const contents = useMemo(() => { + return children + }, [children]) + return ( disableRootThemeClass defaultTheme={rootTheme} > - {children} + {contents} ) } diff --git a/packages/app/provider/theme/UniversalThemeProvider.native.tsx b/packages/app/provider/theme/UniversalThemeProvider.native.tsx index a760d986a..e1f8e6377 100644 --- a/packages/app/provider/theme/UniversalThemeProvider.native.tsx +++ b/packages/app/provider/theme/UniversalThemeProvider.native.tsx @@ -1,13 +1,16 @@ import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native' -import { ThemeProviderProps, useThemeSetting as next_useThemeSetting } from '@tamagui/next-theme' +import type { + ThemeProviderProps, + useThemeSetting as next_useThemeSetting, +} from '@tamagui/next-theme' import { - ThemePreference, + type ThemePreference, setThemePreference, useThemePreference, } from '@vonovak/react-native-theme-control' import { StatusBar } from 'expo-status-bar' import { createContext, useContext, useEffect, useMemo, useState } from 'react' -import { AppState, ColorSchemeName, useColorScheme } from 'react-native' +import { AppState, type ColorSchemeName, useColorScheme } from 'react-native' export const ThemeContext = createContext< (ThemeProviderProps & { current: ThemeName | null }) | null diff --git a/packages/app/provider/toast/ToastProvider.tsx b/packages/app/provider/toast/ToastProvider.tsx index 2b273e0e9..95860b81a 100644 --- a/packages/app/provider/toast/ToastProvider.tsx +++ b/packages/app/provider/toast/ToastProvider.tsx @@ -1,5 +1,5 @@ import { CustomToast, ToastProvider as ToastProviderOG } from '@my/ui' -import { ToastViewport, ToastViewportProps } from './ToastViewport' +import { ToastViewport, type ToastViewportProps } from './ToastViewport' export const ToastProvider = ({ children, diff --git a/packages/app/provider/toast/ToastViewport.native.tsx b/packages/app/provider/toast/ToastViewport.native.tsx index 9094077e3..1aa8b39e6 100644 --- a/packages/app/provider/toast/ToastViewport.native.tsx +++ b/packages/app/provider/toast/ToastViewport.native.tsx @@ -1,6 +1,6 @@ import { ToastViewport as ToastViewportOg } from '@my/ui' import { useSafeAreaInsets } from 'app/utils/useSafeAreaInsets' -import { ToastViewportProps } from './ToastViewport' +import type { ToastViewportProps } from './ToastViewport' export const ToastViewport = ({ noSafeArea }: ToastViewportProps) => { const { top, right, left } = useSafeAreaInsets() diff --git a/packages/app/provider/wagmi/WagmiProvider.native.tsx b/packages/app/provider/wagmi/WagmiProvider.native.tsx new file mode 100644 index 000000000..aebb143de --- /dev/null +++ b/packages/app/provider/wagmi/WagmiProvider.native.tsx @@ -0,0 +1,12 @@ +import { config } from '@my/wagmi' +import type { FC, ReactNode } from 'react' +import { WagmiProvider as OGWagmiProvider } from 'wagmi' + +// use the default config for now (no connectors and no discovery) +export const WagmiProvider: FC<{ children: ReactNode }> = ({ + children, +}: { + children: ReactNode +}) => { + return {children} +} diff --git a/packages/app/provider/wagmi/WagmiProvider.tsx b/packages/app/provider/wagmi/WagmiProvider.tsx index c4da81d3a..33c7387eb 100644 --- a/packages/app/provider/wagmi/WagmiProvider.tsx +++ b/packages/app/provider/wagmi/WagmiProvider.tsx @@ -1,6 +1,33 @@ +import { chains } from '@my/wagmi' +import type { FC, ReactNode } from 'react' import { WagmiProvider as OGWagmiProvider } from 'wagmi' -import { config } from '@my/wagmi' -import { FC, ReactNode } from 'react' + +import { + argentWallet, + trustWallet, + ledgerWallet, + injectedWallet, +} from '@rainbow-me/rainbowkit/wallets' +import { getDefaultWallets, getDefaultConfig } from '@rainbow-me/rainbowkit' + +const { wallets } = getDefaultWallets() + +// use the awesome rainbowkit config on web +export const config = getDefaultConfig({ + appName: '/send', + appIcon: + 'https://github.com/0xsend/sendapp/blob/188fffab9b4d9ab6d332baad09ca14da3308f554/apps/next/public/favicon/apple-touch-icon.png', + projectId: process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID ?? '', + chains, + wallets: [ + ...wallets, + { + groupName: 'Other', + wallets: [injectedWallet, argentWallet, trustWallet, ledgerWallet], + }, + ], + ssr: true, +}) export const WagmiProvider: FC<{ children: ReactNode }> = ({ children, @@ -9,9 +36,3 @@ export const WagmiProvider: FC<{ children: ReactNode }> = ({ }) => { return {children} } - -declare module 'wagmi' { - interface Register { - config: typeof config - } -} diff --git a/packages/app/routers/params.tsx b/packages/app/routers/params.tsx index 1f17343c6..4675c914d 100644 --- a/packages/app/routers/params.tsx +++ b/packages/app/routers/params.tsx @@ -1,6 +1,6 @@ import { createParam } from 'solito' -type Nav = { nav?: 'home' } +type Nav = { nav?: 'home' | 'settings' } const { useParam: useNavParam } = createParam