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'; ```