Skip to content
Merged
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
70 changes: 54 additions & 16 deletions .github/workflows/ci-swift.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ permissions:
on:
push:
paths:
- 'packages/contracts/**'
- 'packages/sdk-platforms/rust/**'
- 'packages/sdk-platforms/swift/**'
- '.github/workflows/ci-swift.yml'
- "packages/contracts/**"
- "packages/sdk-platforms/rust/**"
- "packages/sdk-platforms/swift/**"
- ".github/workflows/ci-swift.yml"
pull_request:
paths:
- 'packages/contracts/**'
- 'packages/sdk-platforms/rust/**'
- 'packages/sdk-platforms/swift/**'
- '.github/workflows/ci-swift.yml'
- "packages/contracts/**"
- "packages/sdk-platforms/rust/**"
- "packages/sdk-platforms/swift/**"
- ".github/workflows/ci-swift.yml"

jobs:
swift-sdk:
Expand All @@ -28,7 +28,7 @@ jobs:
- uses: actions/checkout@v4
with:
submodules: recursive

- name: Run sccache-cache
uses: mozilla-actions/sccache-action@v0.0.4

Expand Down Expand Up @@ -83,22 +83,60 @@ jobs:
- name: Select Xcode 16.3
run: sudo xcode-select -s /Applications/Xcode_16.3.app

- name: Select Simulator
- name: Create and Select Simulator
run: |
UDID=$(xcrun simctl list devices | awk '/-- iOS 18.4 --/{flag=1; next} /--/{flag=0} flag' | grep "iPhone 16 Pro" | awk -F '[()]' '{print $2}' | head -1)
if [ -z "$UDID" ]; then
echo "Simulator not found!" >&2
# List available runtimes before installation
echo "Available runtimes before installation:"
xcrun simctl list runtimes

# Download iOS platform
echo "Downloading iOS platform..."
xcodebuild -downloadPlatform iOS

# Wait for download to complete
sleep 10

# List available runtimes after installation
echo "Available runtimes after installation:"
xcrun simctl list runtimes

# Get the latest iOS runtime
IOS_RUNTIME=$(xcrun simctl list runtimes | grep "iOS" | tail -1 | awk '{for(i=1;i<=NF;i++) if($i ~ /^com\.apple\.CoreSimulator\.SimRuntime/) print $i}' | tr -d ')')

if [ -z "$IOS_RUNTIME" ]; then
echo "ERROR: No iOS runtime found"
xcrun simctl list runtimes
exit 1
fi
echo "Simulator UDID: $UDID"

echo "Using iOS runtime: $IOS_RUNTIME"

# List available device types
echo "Available device types:"
xcrun simctl list devicetypes | grep iPhone

# Create a new iPhone simulator
UDID=$(xcrun simctl create "iPhone-CI" "com.apple.CoreSimulator.SimDeviceType.iPhone-15" "$IOS_RUNTIME")
echo "Created simulator with UDID: $UDID"

# Boot the simulator
xcrun simctl boot "$UDID"
echo "Booted simulator"

# Verify simulator is booted
xcrun simctl list devices | grep "$UDID"

# Wait for simulator to be ready
sleep 5

echo "SIMULATOR_UDID=$UDID" >> $GITHUB_ENV

- name: Install swiftformat
run: brew install swiftformat

- name: Build bindings
run: sh packages/sdk-platforms/rust/zksync-sso/crates/ffi/build-swift-framework-ios-ci.sh

- name: Build & test Swift SDK
run: |
xcodebuild test \
Expand Down
175 changes: 175 additions & 0 deletions examples/demo-app/tests/create-account.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -460,3 +460,178 @@ test("Create account with session, create session via paymaster, and send ETH",

console.log("Session created successfully with balance:", sessionStartBalance, "ETH");
});

test("Create session and verify it appears in auth-server sessions list", async ({ page }) => {
test.setTimeout(120000);
console.log("\n=== Session Display in Sessions List Test ===\n");

// Step 1: Create account
console.log("Step 1: Creating account...");
await page.getByRole("button", { name: "Connect", exact: true }).click();
await page.waitForTimeout(2000);

const popup = page.context().pages()[1];
await expect(popup.getByText("Connect to")).toBeVisible();

// Setup WebAuthn
const client = await popup.context().newCDPSession(popup);
await client.send("WebAuthn.enable");
let newCredential: WebAuthnCredential | null = null;
client.on("WebAuthn.credentialAdded", (credentialAdded) => {
newCredential = credentialAdded.credential;
});
await client.send("WebAuthn.addVirtualAuthenticator", {
options: {
protocol: "ctap2",
transport: "usb",
hasResidentKey: true,
hasUserVerification: true,
isUserVerified: true,
automaticPresenceSimulation: true,
},
});

// Complete signup
await popup.getByTestId("signup").click();
await expect(popup.getByText("Connect to ZKsync SSO Demo")).toBeVisible();
await popup.getByTestId("connect").click();
await page.waitForTimeout(2000);
await expect(page.getByText("Disconnect")).toBeVisible();

// Capture the account address from the page
const demoPageContent = await page.textContent("body");
const accountMatch = demoPageContent?.match(/0x[a-fA-F0-9]{40}/);
const demoAccountAddress = accountMatch ? accountMatch[0] : "unknown";
console.log(`✓ Account created: ${demoAccountAddress}`);

// Step 2: Create session
console.log("\nStep 2: Creating session...");
await page.getByRole("button", { name: "Disconnect", exact: true }).click();
await expect(page.getByRole("button", { name: "Connect with Session", exact: true })).toBeVisible();
await page.getByRole("button", { name: "Connect with Session", exact: true }).click();
await page.waitForTimeout(2000);

const sessionPopup = page.context().pages()[1];
await expect(sessionPopup.getByText("Act on your behalf")).toBeVisible();

// Setup WebAuthn with existing credential
const sessionClient = await sessionPopup.context().newCDPSession(sessionPopup);
await sessionClient.send("WebAuthn.enable");
const sessionAuthenticator = await sessionClient.send("WebAuthn.addVirtualAuthenticator", {
options: {
protocol: "ctap2",
transport: "usb",
hasResidentKey: true,
hasUserVerification: true,
isUserVerified: true,
automaticPresenceSimulation: true,
},
});
await expect(newCredential).not.toBeNull();
await sessionClient.send("WebAuthn.addCredential", {
authenticatorId: sessionAuthenticator.authenticatorId,
credential: newCredential!,
});

// Authorize session
await expect(sessionPopup.getByText("Authorize ZKsync SSO Demo")).toBeVisible();
await sessionPopup.getByTestId("connect").click();
await page.waitForTimeout(3000);
await expect(page.getByText("Disconnect")).toBeVisible();
console.log("✓ Session created");

// Step 3: Navigate to auth-server sessions page to verify
console.log("\nStep 3: Verifying session appears in auth-server...");

const authPage = await page.context().newPage();
await authPage.goto("http://localhost:3002");
await authPage.waitForTimeout(1000);

// Check if logged in
const isLoggedIn = await authPage.locator("[data-testid='account-address']").isVisible({ timeout: 2000 }).catch(() => false);

if (!isLoggedIn) {
console.log("Logging into auth-server...");
// Already on auth-server homepage, just click login
await authPage.getByTestId("login").click();
await authPage.waitForTimeout(1000);

// Setup WebAuthn for login
const authClient = await authPage.context().newCDPSession(authPage);
await authClient.send("WebAuthn.enable");
const authAuthenticator = await authClient.send("WebAuthn.addVirtualAuthenticator", {
options: {
protocol: "ctap2",
transport: "usb",
hasResidentKey: true,
hasUserVerification: true,
isUserVerified: true,
automaticPresenceSimulation: true,
},
});
await authClient.send("WebAuthn.addCredential", {
authenticatorId: authAuthenticator.authenticatorId,
credential: newCredential!,
});

await authPage.waitForURL("**/dashboard", { timeout: 15000 });
console.log("✓ Logged into auth-server");
}

// Navigate to sessions page
await authPage.goto("http://localhost:3002/dashboard/sessions");
await authPage.waitForLoadState("domcontentloaded");

// Listen for console logs from the sessions page
authPage.on("console", (msg) => {
if (msg.text().includes("[sessions.vue]")) {
console.log(` Auth-server: ${msg.text()}`);
}
});

console.log("✓ Navigated to sessions page");
console.log(` Demo account (created session): ${demoAccountAddress}`);

// Verify sessions page content
const header = authPage.locator("header").getByText("Sessions");
await expect(header).toBeVisible();
console.log("✓ Sessions page loaded");

// Wait for sessions data to load - look for either session rows or the table/list container
// The sessions are loaded via WASM asynchronously, so we need to wait
try {
// Wait for the sessions list container or session rows to appear
await authPage.waitForSelector("table tbody tr, [role='list'] > div, [data-testid*='session']", {
timeout: 15000,
state: "attached",
});
console.log("✓ Sessions data container loaded");
} catch (e) {
console.log("⚠ No sessions container appeared within 15s", e);
}

// Additional wait to ensure console logs are captured
await authPage.waitForTimeout(2000);

// Log page content for debugging
const pageContent = await authPage.locator("main").textContent();
console.log(`Page content: ${pageContent?.substring(0, 500)}`);

// Verify at least one session is displayed
// The session rows use class="session-row" in the SessionRow component
const sessionRows = authPage.locator(".session-row");
const sessionCount = await sessionRows.count();
console.log(`Found ${sessionCount} session row(s)`);

expect(sessionCount, "At least one session should be displayed").toBeGreaterThan(0);
console.log(`✓ Found ${sessionCount} session(s) displayed`);

// Verify empty state message is NOT shown
const emptyState = authPage.getByText(/no active sessions/i);
const emptyVisible = await emptyState.isVisible({ timeout: 1000 }).catch(() => false);
expect(emptyVisible, "Empty state should NOT be visible when sessions exist").toBe(false);
console.log("✓ Empty state correctly hidden");

await authPage.close();
console.log("\n=== Session Display Test Complete ===\n");
});
9 changes: 6 additions & 3 deletions packages/auth-server/components/session/row/Expiry.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ const props = defineProps<{
status: SessionStatus;
isExpired: boolean;
now: number;
createdAt: number;
expiresAt: number;
maxExpiresAt: number;
}>();

const expiresIn = useTimeAgo(props.expiresAt, { showSecond: true, updateInterval: 1000 });
Expand All @@ -54,7 +54,10 @@ const sessionExpiry = computed(() => {
});
});
const timeLeft = computed<number>(() => Math.max(0, props.expiresAt - props.now));
const timeTotal = computed<number>(() => Math.max(0, props.expiresAt - props.createdAt));
const timeLeftPercentage = computed<number>(() => Math.min(100, (timeLeft.value / timeTotal.value) * 100));
const maxTimeLeft = computed<number>(() => Math.max(0, props.maxExpiresAt - props.now));
const timeLeftPercentage = computed<number>(() => {
if (maxTimeLeft.value === 0) return 0;
return Math.min(100, (timeLeft.value / maxTimeLeft.value) * 100);
});
const isRevoked = computed(() => props.status === SessionStatus.Closed);
</script>
Loading
Loading