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
1 change: 1 addition & 0 deletions okf-bundle/ci-workflows/android.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,6 @@ Under load, Jet may run only a small prefix before mocha-remote desync, often af
| Empty Jacoco XML (~235 bytes) | No `.ec` pulled — check post-e2e logs |
| `adb reverse --remove` in Detox logs | Expected on 1006; should be warn-only after Detox patch |
| Detox red, tests green in log | Pre-patch: teardown adb error; re-run or check patch applied |
| Emulator offline / hung / duplicate instance | Warm quickboot snapshot restore; `tests/.detoxrc.js` sets `bootArgs: '-no-snapshot-load -no-snapshot-save'` for cold boot when Detox launches TestingAVD |
| `codecov/project/android-native` fail | Jacoco XML not uploaded — check post-e2e logs and Codecov Uploads tab for `android-native` flag |
| FIS 503 / `Too many server requests` / RC cascade | Live cloud quota (shared project) — not Android-specific; see [cloud API quota triage](../testing/firebase-testing-project.md#ci-triage-cloud-api-quota-pressure) |
11 changes: 11 additions & 0 deletions okf-bundle/testing/change-authoring-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,17 @@ flowchart TD

Step detail: [running e2e § unit-focused iteration loop](running-e2e.md#unit-focused-tier-iteration-loop).

<a id="platform-sdk-bridge-contracts"></a>

### Platform SDK bridge contracts (blocking)

Before changing native bridge code that calls a **platform SDK** (Firebase Android/iOS, OS APIs, vendor SDKs, etc.):

1. **Read each platform's official API signature and docs independently** — do not assume Android, iOS, and Web behave the same because the RNFB bridge presents a unified JavaScript surface.
2. **Verify null vs empty string** — many SDKs treat absent values (`null`/`nil`) and empty strings (`""`) differently; map bridge fields to the SDK parameter the docs specify for "absent" values.
3. **Do not apply defensive parity** — fixing platform A does not justify the same change on platform B without checking B's contract (e.g. `@Nullable` vs `nonnull`, optional vs required).
4. **Record evidence** — link or cite the reference URL / header signature in the work queue note or triage report when the fix is native-only on one platform.

<a id="e2e-diagnosis-escalation"></a>

### E2e diagnosis escalation
Expand Down
20 changes: 20 additions & 0 deletions packages/auth/__tests__/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,26 @@ describe('Auth', function () {
expect(credential?.idToken).toBe('id-token');
});

it('GoogleAuthProvider.credential maps id-token-only credentials for native bridge', function () {
const credential = GoogleAuthProvider.credential('google-id-token', null);
expect(credential.providerId).toBe('google.com');
expect(credential.signInMethod).toBe('google.com');
expect(credential.idToken).toBe('google-id-token');
expect(credential.accessToken).toBeUndefined();
expect(credential.token).toBe('google-id-token');
expect(credential.secret).toBe('');
});

it('GoogleAuthProvider.credential maps access-token-only credentials for native bridge', function () {
const credential = GoogleAuthProvider.credential(null, 'google-access-token');
expect(credential.providerId).toBe('google.com');
expect(credential.signInMethod).toBe('google.com');
expect(credential.accessToken).toBe('google-access-token');
expect(credential.idToken).toBeUndefined();
expect(credential.token).toBe('');
expect(credential.secret).toBe('google-access-token');
});

it('PhoneAuthCredential.fromJSON deserializes phone credentials', function () {
const credential = PhoneAuthCredential.fromJSON({
verificationId: 'vid',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2046,7 +2046,12 @@ private AuthCredential getCredentialForProvider(
case "facebook.com":
return FacebookAuthProvider.getCredential(authToken);
case "google.com":
return GoogleAuthProvider.getCredential(authToken, authSecret);
// Firebase Android rejects empty tokens; id-token-only and access-token-only flows require
// null.
// https://firebase.google.com/docs/reference/android/com/google/firebase/auth/GoogleAuthProvider#getCredential(java.lang.String,java.lang.String)
String googleIdToken = (authToken == null || authToken.isEmpty()) ? null : authToken;
String googleAccessToken = (authSecret == null || authSecret.isEmpty()) ? null : authSecret;
return GoogleAuthProvider.getCredential(googleIdToken, googleAccessToken);
case "twitter.com":
return TwitterAuthProvider.getCredential(authToken, authSecret);
case "github.com":
Expand Down
64 changes: 64 additions & 0 deletions packages/auth/e2e/provider.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,70 @@ describe('auth() -> Providers', function () {
'At least one of ID token and access token must be non-null',
);
});

it('should accept id-token-only credentials (Credential Manager)', function () {
const { GoogleAuthProvider } = authModular;

const token = 'google-id-token-only';
const credential = GoogleAuthProvider.credential(token, null);
credential.providerId.should.equal('google.com');
credential.signInMethod.should.equal('google.com');
credential.token.should.equal(token);
credential.secret.should.equal('');
credential.idToken.should.equal(token);
should.equal(credential.accessToken, undefined);
});

it('should accept access-token-only credentials', function () {
const { GoogleAuthProvider } = authModular;

const accessToken = 'google-access-token-only';
const credential = GoogleAuthProvider.credential(null, accessToken);
credential.providerId.should.equal('google.com');
credential.signInMethod.should.equal('google.com');
credential.token.should.equal('');
credential.secret.should.equal(accessToken);
credential.accessToken.should.equal(accessToken);
should.equal(credential.idToken, undefined);
});

it('should reach Android native signInWithCredential for id-token-only Google credentials', async function () {
if (!Platform.android) {
this.skip();
}
const { getApp } = modular;
const { GoogleAuthProvider, signInWithCredential, getAuth } = authModular;
const defaultAuth = getAuth(getApp());
const credential = GoogleAuthProvider.credential('google-id-token-only-e2e', null);

try {
await signInWithCredential(defaultAuth, credential);
throw new Error('Did not error.');
} catch (error) {
error.code.should.be.a.String();
error.code.should.not.equal('auth/unknown');
String(error.message).should.not.match(/accessToken cannot be empty/i);
}
});

it('should reach Android native signInWithCredential for access-token-only Google credentials', async function () {
if (!Platform.android) {
this.skip();
}
const { getApp } = modular;
const { GoogleAuthProvider, signInWithCredential, getAuth } = authModular;
const defaultAuth = getAuth(getApp());
const credential = GoogleAuthProvider.credential(null, 'google-access-token-only-e2e');

try {
await signInWithCredential(defaultAuth, credential);
throw new Error('Did not error.');
} catch (error) {
error.code.should.be.a.String();
error.code.should.not.equal('auth/unknown');
String(error.message).should.not.match(/idToken cannot be empty/i);
}
});
});

describe('GOOGLE_SIGN_IN_METHOD', function () {
Expand Down
3 changes: 3 additions & 0 deletions tests/.detoxrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ module.exports = {
device: {
avdName: 'TestingAVD',
},
// Cold boot: do not load/save AVD snapshots (warm quickboot is unreliable locally).
bootArgs: '-no-snapshot-load -no-snapshot-save',
readonly: true,
},
},
configurations: {
Expand Down
Loading