Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm install --save-dev @commitlint/cli @commitlint/config-conventional
- name: Lint commits in PR
run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ jobs:
working-directory: .
run: |
NOTES=$(npx --yes conventional-changelog-cli -p angular -r 1 --outfile /dev/stdout 2>/dev/null)
# Store as multiline env var
{
echo "RELEASE_NOTES<<EOF"
echo "$NOTES"
echo "EOF"
# Store as multiline env var using a random delimiter to avoid EOF injection
DELIM=$(openssl rand -hex 8)
{
Expand All @@ -83,6 +88,8 @@ jobs:
git config user.email "github-actions[bot]@users.noreply.github.com"
git add CHANGELOG.md
# Only commit if there are staged changes
git diff --cached --quiet || git commit -m "chore: update CHANGELOG.md for ${{ github.ref_name }} [skip ci]"
git push origin HEAD:main
if ! git diff --cached --quiet; then
git commit -m "chore: update CHANGELOG.md for ${{ github.ref_name }} [skip ci]"
# Fetch the latest main and retry rebase/push in case of concurrent merges
Expand Down
1 change: 1 addition & 0 deletions commitlint.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
module.exports = {
extends: ['@commitlint/config-conventional']
extends: ['@commitlint/config-conventional'],
rules: {
'body-max-line-length': [0],
Expand Down
3 changes: 3 additions & 0 deletions commitlint.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": ["@commitlint/config-conventional"]
}
97 changes: 90 additions & 7 deletions frontend/app/apps/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { Suspense, useState } from "react";
import Link from "next/link";
import { useParams, useSearchParams } from "next/navigation";
import { useTranslations } from "next-intl";
import {
IconLock,
IconCheck,
Expand All @@ -22,6 +23,69 @@ import { QrCodeModal } from "@/components/QrCodeModal";
import { getProtocol, type Protocol } from "@/lib/protocols";
import { useProtocolAccessCheck } from "@/lib/use-protocol-access";

function ProtocolDetailInner() {
const { id } = useParams<{ id: string }>();
const { address } = useWallet();
const searchParams = useSearchParams();
const t = useTranslations("apps");

const scVerified = searchParams.get("sc_verified") === "true";
const scWallet = searchParams.get("sc_wallet");
const activeWallet = address ?? scWallet ?? null;
const isPreview = usePreviewMode();

const protocol = getProtocol(id);

const [statuses, setStatuses] = useState<boolean[]>([]);
const [checked, setChecked] = useState(false);
const [inputValue, setInputValue] = useState(protocol?.inputDefault ?? "");
const [showQr, setShowQr] = useState(false);

useEffect(() => {
if (!protocol) return;
setStatuses(protocol.requirements.map(() => false));
}, [protocol]);

useEffect(() => {
if (isPreview && protocol) {
setChecked(true);
setStatuses(protocol.requirements.map(() => true));
return;
}
if (!activeWallet || !protocol) {
setChecked(false);
setStatuses(protocol?.requirements.map(() => false) ?? []);
return;
}
let cancelled = false;
(async () => {
try {
const results = await Promise.all(
protocol.requirements.map((r) => checkClaim(activeWallet, r.type, r.minThreshold)),
);
if (!cancelled) setStatuses(results);
} catch {
// contracts not deployed — requirements stay unmet
} finally {
if (!cancelled) setChecked(true);
}
})();
return () => { cancelled = true; };
}, [activeWallet, protocol]);

if (!protocol) {
return (
<div style={{ textAlign: "center", padding: "4rem 0" }}>
<p className="muted">{t("protocolNotFound")}</p>
<Link href="/apps" className="btn btn-secondary btn-sm" style={{ marginTop: "1rem" }}>
<IconArrowLeft size={14} /> {t("backToApps")}
</Link>
</div>
);
}

const eligible = statuses.length > 0 && statuses.every(Boolean);

function ProtocolDetailBody({
protocol,
activeWallet,
Expand Down Expand Up @@ -58,7 +122,7 @@ function ProtocolDetailBody({
textDecoration: "none",
}}
>
<IconArrowLeft size={13} /> Apps
<IconArrowLeft size={13} /> {t("backToApps")}
</Link>
<div className="row" style={{ gap: "0.6rem", alignItems: "center" }}>
<span style={{ color: "var(--accent)" }}>{protocol.icon}</span>
Expand Down Expand Up @@ -89,8 +153,8 @@ function ProtocolDetailBody({
>
<IconCheck size={18} color="var(--accent)" stroke={2.5} />
<span>
<strong>Verification complete.</strong>{" "}
<span className="muted">You were returned here from StellarCred automatically.</span>
<strong>{t("verificationComplete")}</strong>{" "}
<span className="muted">{t("returnedFrom")}</span>
</span>
</div>
)}
Expand Down Expand Up @@ -126,6 +190,7 @@ function ProtocolDetailBody({
</div>
</div>

<span className="eyebrow" style={{ marginBottom: "0.4rem", display: "block" }}>{t("requirements")}</span>
<span className="eyebrow" style={{ marginBottom: "0.4rem", display: "block" }}>
Requirements
</span>
Expand Down Expand Up @@ -154,19 +219,29 @@ function ProtocolDetailBody({
{r.label}
</span>
</span>
{statuses[i] ? (
<Badge variant="verified">{t("proved")}</Badge>
{checking ? (
<Badge variant="pending">Checking</Badge>
) : state === "error" ? (
<Badge variant="denied">Unavailable</Badge>
) : statuses[i] ? (
<Badge variant="verified">Proved</Badge>
) : (
<Badge variant="pending">Needed</Badge>
<Badge variant="pending">{t("needed")}</Badge>
)}
</div>
))}
</div>

{checked && !eligible && (
<Link href={protocol.verifyUrl} className="btn btn-secondary" style={{ width: "100%" }}>
{t("getVerified")}
<IconArrowRight size={14} />
</Link>
)}
{!activeWallet && (
{checked && !eligible && !isPreview && (
{state === "error" && (
<button
type="button"
Expand Down Expand Up @@ -215,7 +290,7 @@ function ProtocolDetailBody({

{!activeWallet && (
<p className="faint" style={{ marginTop: "0.75rem", fontSize: "0.8rem" }}>
Connect your wallet to check eligibility.
{t("connectToCheck")}
</p>
)}
</div>
Expand All @@ -229,6 +304,10 @@ function ProtocolDetailBody({
>
<div className="between" style={{ marginBottom: "1.5rem" }}>
<span className="eyebrow">{protocol.actionLabel}</span>
{checked && (
eligible
? <Badge variant="verified">{t("accessGranted")}</Badge>
: <Badge variant="denied">{t("accessDenied")}</Badge>
{state === "loading" && (
<span className="row faint" style={{ gap: "0.35rem", fontSize: "0.75rem" }}>
<IconLoader2 size={14} className="spin" />
Expand Down Expand Up @@ -261,6 +340,10 @@ function ProtocolDetailBody({
}}
disabled={!eligible || isPreview}
>
{eligible ? protocol.actionLabel : (
<><IconLock size={14} /> {t("proveFirst")}</>
{isPreview ? "Connect wallet to check access" : eligible ? protocol.actionLabel : (
<><IconLock size={14} /> Prove eligibility first</>
{isPreview ? (
"Connect wallet to check access"
) : eligible ? (
Expand All @@ -274,8 +357,8 @@ function ProtocolDetailBody({

<p className="faint" style={{ marginTop: "1.25rem", fontSize: "0.8125rem", lineHeight: 1.6 }}>
{eligible
? `${protocol.name} read ProofRegistry.check_claim and found valid proofs for your address. No personal data was shared.`
: `${protocol.name} only reads ProofRegistry.check_claim for your address — it never sees the credential data behind your proofs.`}
? t("noPersonalData", { name: protocol.name })
: t("noPersonalDataDenied", { name: protocol.name })}
</p>
</div>
</div>
Expand Down
Loading
Loading