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
4 changes: 2 additions & 2 deletions app/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,8 @@ android {
applicationId "com.proofofpassportapp"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 81
versionName "2.6.0"
versionCode 82
versionName "2.6.1"
manifestPlaceholders = [appAuthRedirectScheme: 'com.proofofpassportapp']
externalNativeBuild {
cmake {
Expand Down
9 changes: 9 additions & 0 deletions app/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="redirect.self.xyz" />
</intent-filter>

<meta-data
android:name="android.nfc.action.TECH_DISCOVERED"
android:resource="@xml/nfc_tech_filter" />
Expand Down
182 changes: 182 additions & 0 deletions app/tests/src/androidManifest.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11

/**
* @jest-environment node
*/

import * as fs from 'fs';
import * as path from 'path';

describe('Android Manifest Configuration', () => {
const manifestPath = path.join(
__dirname,
'../../android/app/src/main/AndroidManifest.xml',
);
let manifestContent: string;

beforeAll(() => {
// Read the manifest file
manifestContent = fs.readFileSync(manifestPath, 'utf8');
});

describe('Critical Deeplink Configuration', () => {
it('should contain the redirect.self.xyz deeplink intent filter', () => {
// This is the configuration that was accidentally deleted
expect(manifestContent).toContain('android:host="redirect.self.xyz"');
expect(manifestContent).toContain('android:autoVerify="true"');
expect(manifestContent).toContain('android.intent.action.VIEW');
expect(manifestContent).toContain('android.intent.category.BROWSABLE');
expect(manifestContent).toContain('android:scheme="https"');
});

it('should have the deeplink intent filter in the MainActivity', () => {
// Ensure the deeplink is properly configured in the main activity
const mainActivityMatch = manifestContent.match(
/<activity[^>]*android:name="\.MainActivity"[^>]*>(.*?)<\/activity>/s,
);

expect(mainActivityMatch).toBeTruthy();
expect(mainActivityMatch![1]).toContain('redirect.self.xyz');
expect(mainActivityMatch![1]).toContain('android:autoVerify="true"');
});
});

describe('Firebase Configuration', () => {
it('should have Firebase Messaging Service configured', () => {
expect(manifestContent).toContain(
'com.google.firebase.messaging.FirebaseMessagingService',
);
expect(manifestContent).toContain('com.google.firebase.MESSAGING_EVENT');
});

it('should have Firebase metadata configurations', () => {
const firebaseMetaConfigs = [
'com.google.firebase.messaging.default_notification_channel_id',
'com.google.firebase.messaging.default_notification_icon',
'com.google.firebase.messaging.default_notification_color',
];

firebaseMetaConfigs.forEach(config => {
expect(manifestContent).toContain(`android:name="${config}"`);
});
});

it('should have Firebase service properly exported', () => {
// Firebase service should not be exported for security
const serviceMatch = manifestContent.match(
/<service[^>]*android:name="com\.google\.firebase\.messaging\.FirebaseMessagingService"[^>]*>/,
);
expect(serviceMatch).toBeTruthy();
expect(serviceMatch![0]).toContain('android:exported="false"');
});
});

describe('OAuth/AppAuth Configuration', () => {
it('should have AppAuth RedirectUriReceiverActivity configured', () => {
expect(manifestContent).toContain(
'net.openid.appauth.RedirectUriReceiverActivity',
);
expect(manifestContent).toContain('${appAuthRedirectScheme}');
expect(manifestContent).toContain('oauth2redirect');
});

it('should have OAuth activity properly exported', () => {
const oauthActivityMatch = manifestContent.match(
/<activity[^>]*android:name="net\.openid\.appauth\.RedirectUriReceiverActivity"[^>]*>/,
);
expect(oauthActivityMatch).toBeTruthy();
expect(oauthActivityMatch![0]).toContain('android:exported="true"');
});
});

describe('NFC Configuration', () => {
it('should have NFC permission', () => {
expect(manifestContent).toContain('android.permission.NFC');
});

it('should have NFC tech discovery metadata', () => {
expect(manifestContent).toContain('android.nfc.action.TECH_DISCOVERED');
expect(manifestContent).toContain('@xml/nfc_tech_filter');
});
});

describe('Required Permissions', () => {
const criticalPermissions = [
'android.permission.INTERNET',
'android.permission.CAMERA',
'android.permission.NFC',
'android.permission.VIBRATE',
'android.permission.POST_NOTIFICATIONS',
'android.permission.ACCESS_SURFACE_FLINGER',
'android.permission.RECEIVE_BOOT_COMPLETED',
];

criticalPermissions.forEach(permission => {
it(`should contain ${permission} permission`, () => {
expect(manifestContent).toContain(`android:name="${permission}"`);
});
});
});

describe('Main Activity Configuration', () => {
it('should have MainActivity properly configured', () => {
expect(manifestContent).toContain('android:name=".MainActivity"');
expect(manifestContent).toContain('android:exported="true"');
expect(manifestContent).toContain('android:launchMode="singleTop"');
expect(manifestContent).toContain('android:screenOrientation="portrait"');
});

it('should have main launcher intent filter', () => {
expect(manifestContent).toContain('android.intent.action.MAIN');
expect(manifestContent).toContain('android.intent.category.LAUNCHER');
});

it('should have proper config changes handled', () => {
const configChanges = [
'keyboard',
'keyboardHidden',
'orientation',
'screenLayout',
'screenSize',
'smallestScreenSize',
'uiMode',
];

configChanges.forEach(change => {
expect(manifestContent).toContain(change);
});
});
});

describe('Application Configuration', () => {
it('should have MainApplication configured', () => {
expect(manifestContent).toContain('android:name=".MainApplication"');
expect(manifestContent).toContain('android:largeHeap="true"');
expect(manifestContent).toContain('android:supportsRtl="true"');
});

it('should have proper theme and icons configured', () => {
expect(manifestContent).toContain('@style/AppTheme');
expect(manifestContent).toContain('@mipmap/ic_launcher');
});
});

describe('Manifest Structure Validation', () => {
it('should be valid XML structure', () => {
// Basic XML validation - ensure it has proper opening/closing tags
expect(manifestContent).toMatch(/^<manifest[^>]*>/);
expect(manifestContent).toContain('</manifest>');
expect(manifestContent).toContain('<application');
expect(manifestContent).toContain('</application>');
});

it('should have required namespaces', () => {
expect(manifestContent).toContain(
'xmlns:android="http://schemas.android.com/apk/res/android"',
);
expect(manifestContent).toContain(
'xmlns:tools="http://schemas.android.com/tools"',
);
});
});
});
10 changes: 5 additions & 5 deletions sdk/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import { DefaultConfigStore, InMemoryConfigStore } from '@selfxyz/core';
const configStore = new DefaultConfigStore({
minimumAge: 18,
excludedCountries: ['IRN', 'PRK', 'RUS', 'SYR'],
ofac: true
ofac: true,
});

// For dynamic config management
Expand Down Expand Up @@ -117,15 +117,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)

if (!attestationId || !proof || !publicSignals || !userContextData) {
return res.status(400).json({
message: 'attestationId, proof, publicSignals, and userContextData are required'
message: 'attestationId, proof, publicSignals, and userContextData are required',
});
}

// Create configuration storage
const configStore = new DefaultConfigStore({
minimumAge: 18,
excludedCountries: ['IRN', 'PRK', 'RUS', 'SYR'],
ofac: true
ofac: true,
});

// Initialize the verifier
Expand Down Expand Up @@ -194,7 +194,7 @@ const iranName = countryCodes.IRN; // "Iran (Islamic Republic of)"
// Use in configuration
const configStore = new DefaultConfigStore({
excludedCountries: [countries.IRAN, countries.NORTH_KOREA, countries.SYRIA],
ofac: true
ofac: true,
});
```

Expand Down Expand Up @@ -237,7 +237,7 @@ import {
AttestationId,
VerificationResult,
VerificationConfig,
IConfigStorage
IConfigStorage,
} from '@selfxyz/core';
```

Expand Down
Loading