diff --git a/app/android/app/build.gradle b/app/android/app/build.gradle
index d6cfe7791..878a832f8 100644
--- a/app/android/app/build.gradle
+++ b/app/android/app/build.gradle
@@ -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 {
diff --git a/app/android/app/src/main/AndroidManifest.xml b/app/android/app/src/main/AndroidManifest.xml
index 712911ace..2616d1dba 100644
--- a/app/android/app/src/main/AndroidManifest.xml
+++ b/app/android/app/src/main/AndroidManifest.xml
@@ -36,6 +36,15 @@
+
+
+
+
+
+
+
diff --git a/app/tests/src/androidManifest.test.ts b/app/tests/src/androidManifest.test.ts
new file mode 100644
index 000000000..b132cb19e
--- /dev/null
+++ b/app/tests/src/androidManifest.test.ts
@@ -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(
+ /]*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(
+ /]*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(
+ /]*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(/^]*>/);
+ expect(manifestContent).toContain('');
+ expect(manifestContent).toContain('');
+ });
+
+ 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"',
+ );
+ });
+ });
+});
diff --git a/sdk/core/README.md b/sdk/core/README.md
index d75ef7b13..0530d4b56 100644
--- a/sdk/core/README.md
+++ b/sdk/core/README.md
@@ -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
@@ -117,7 +117,7 @@ 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',
});
}
@@ -125,7 +125,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const configStore = new DefaultConfigStore({
minimumAge: 18,
excludedCountries: ['IRN', 'PRK', 'RUS', 'SYR'],
- ofac: true
+ ofac: true,
});
// Initialize the verifier
@@ -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,
});
```
@@ -237,7 +237,7 @@ import {
AttestationId,
VerificationResult,
VerificationConfig,
- IConfigStorage
+ IConfigStorage,
} from '@selfxyz/core';
```