Skip to content
Open
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
17 changes: 12 additions & 5 deletions packages/flutterfire_cli/lib/src/commands/reconfigure.dart
Original file line number Diff line number Diff line change
Expand Up @@ -107,18 +107,25 @@ class Reconfigure extends FlutterFireCommand {
Map<String, dynamic> configuration,
String platform,
) async {
// We pass access token for CI as the token won't be present in the "firebase-tools.json" file unless you login
accessToken ??= await getAccessToken();

final serviceFilePath = configuration[kFileOutput] as String;
// ignore: cast_nullable_to_non_nullable
final projectId = configuration[kProjectId] as String;
// ignore: cast_nullable_to_non_nullable
final appId = configuration[kAppId] as String;

if (platform == kAndroid || platform == kIos || platform == kMacos) {
final serviceFileContent =
await getServiceFileContent(projectId, appId, accessToken!, platform);
// Go through the Firebase CLI so we use however it is authenticated
// (`firebase login`, `FIREBASE_TOKEN`, application default credentials).
// An access token is only passed explicitly for CI.
final serviceFileContent = accessToken != null
? await getServiceFileContent(
projectId,
appId,
accessToken!,
platform,
)
: (await getAppSdkConfig(appId: appId, platform: platform))
.fileContents;

final serviceFilePathAbsolute =
File(path.join(flutterApp!.package.path, serviceFilePath));
Expand Down
38 changes: 0 additions & 38 deletions packages/flutterfire_cli/lib/src/firebase.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import 'dart:io';

import 'package:ansi_styles/ansi_styles.dart';
import 'package:http/http.dart' as http;
import 'package:path/path.dart' as path;

import 'common/global.dart';
import 'common/strings.dart';
Expand Down Expand Up @@ -512,43 +511,6 @@ Future<FirebaseApp> createAppleApp({
});
}

Future<String> getAccessToken() async {
// Use refresh token to get access token, cannot simply use access token found in "firebase-tools.json"
final homeDir = Platform.isWindows
? Platform.environment['UserProfile']!
: Platform.environment['HOME']!;
// Path to 'firebase-tools.json'
final configPath =
path.join(homeDir, '.config', 'configstore', 'firebase-tools.json');
final configFile = File(configPath);
if (!configFile.existsSync()) {
throw Exception(
'Failed to find "firebase-tools.json" file, it should be located at "$configPath',
);
}
final map = await configFile.readAsString();
final configJson = jsonDecode(map) as Map<String, dynamic>;
final tokens = configJson['tokens'] as Map<String, dynamic>;
final refreshToken = tokens['refresh_token'] as String;

final response = await http.post(
Uri.parse('https://oauth2.googleapis.com/token'),
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
// Values for obtaining the access token are taken from the Firebase CLI source code: https://github.com/firebase/firebase-tools/blob/b14b5f38fe23da6543778a588811b0e2391427c0/src/api.ts#L18
body:
'grant_type=refresh_token&client_id=563584335869-fgrhgmd47bqnekij5i8b5pr03ho849e6.apps.googleusercontent.com&client_secret=j9iVZfS8kkCEFUPaAeJV0sAi&refresh_token=$refreshToken',
);

if (response.statusCode == 200) {
final json = jsonDecode(response.body) as Map<String, dynamic>;
return json['access_token'] as String;
} else {
throw Exception(
'Failed to obtain an access token for making Firebase Management REST API requests. Status code: ${response.statusCode}. Response body: ${response.body}',
);
}
}

// Return string value of "GoogleService-Info.plist" or "google-services.json" file for relevant platform
Future<String> getServiceFileContent(
String projectId,
Expand Down
162 changes: 162 additions & 0 deletions packages/flutterfire_cli/test/reconfigure_service_file_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// The stub Firebase CLI below is a shell script.
@TestOn('mac-os || linux')
library;

import 'dart:convert';
import 'dart:io';

import 'package:path/path.dart' as path;
import 'package:test/test.dart';

const _appId = '1:123:android:abc';
const _serviceFileContents = '{"project_info":{"project_id":"fake-project"}}';

/// Runs `flutterfire reconfigure` on an Android-only app, against a stub
/// `firebase` on the PATH that is authenticated without `firebase login`,
/// i.e. with no refresh token in the configstore.
///
/// Returns the arguments each stub `firebase` call received.
Future<List<String>> _reconfigure(
Directory root, {
required void Function(Directory home) setUpHome,
}) async {
final app = Directory(path.join(root.path, 'app'))..createSync();
File(path.join(app.path, 'pubspec.yaml')).writeAsStringSync('''
name: test_app
environment:
sdk: ">=3.0.0 <4.0.0"
dependencies:
flutter:
sdk: flutter
''');
File(path.join(app.path, 'firebase.json')).writeAsStringSync(
jsonEncode({
'flutter': {
'platforms': {
'android': {
'default': {
'projectId': 'fake-project',
'appId': _appId,
'fileOutput': 'android/app/google-services.json',
},
},
},
},
}),
);
// Gradle files that already apply the Google Services plugin, so
// reconfigure has nothing to change in them.
final android = Directory(path.join(app.path, 'android'));
Directory(path.join(android.path, 'app')).createSync(recursive: true);
File(path.join(android.path, 'build.gradle.kts')).writeAsStringSync('');
File(path.join(android.path, 'settings.gradle.kts')).writeAsStringSync('''
plugins {
id("com.android.application") version "8.7.0" apply false
// START: FlutterFire Configuration
id("com.google.gms.google-services") version("4.3.15") apply false
// END: FlutterFire Configuration
}
''');
File(path.join(android.path, 'app', 'build.gradle.kts')).writeAsStringSync('''
plugins {
id("com.android.application")
// START: FlutterFire Configuration
id("com.google.gms.google-services")
// END: FlutterFire Configuration
}
''');

final bin = Directory(path.join(root.path, 'bin'))..createSync();
final calls = File(path.join(root.path, 'calls.log'));
final sdkConfig = jsonEncode({
'status': 'success',
'result': {
'fileName': 'google-services.json',
'fileContents': _serviceFileContents,
},
});
final firebase = File(path.join(bin.path, 'firebase'))..writeAsStringSync('''
#!/bin/sh
echo "\$@" >> '${calls.path}'
case "\$1" in
--version) echo 14.0.0 ;;
apps:sdkconfig) printf '%s' '$sdkConfig' ;;
*) echo '{"status":"error","error":"unexpected command"}'; exit 1 ;;
esac
''');
Process.runSync('chmod', ['+x', firebase.path]);

final home = Directory(path.join(root.path, 'home'))..createSync();
setUpHome(home);

final result = await Process.run(
Platform.resolvedExecutable,
[
path.join(Directory.current.path, 'bin', 'flutterfire.dart'),
'reconfigure',
],
workingDirectory: app.path,
environment: {
'HOME': home.path,
'PATH': '${bin.path}:${Platform.environment['PATH']}',
},
);
expect(result.exitCode, 0, reason: '${result.stdout}\n${result.stderr}');

final serviceFile =
File(path.join(app.path, 'android', 'app', 'google-services.json'));
expect(
serviceFile.existsSync(),
isTrue,
reason: 'google-services.json was not written:\n${result.stdout}',
);
expect(serviceFile.readAsStringSync(), _serviceFileContents);

return calls.readAsLinesSync();
}

void main() {
late Directory root;

setUp(() {
root = Directory.systemTemp.createTempSync('flutterfire_reconfigure');
});

tearDown(() {
root.deleteSync(recursive: true);
});

test(
'reconfigure fetches the service file through the Firebase CLI when there '
'is no configstore file',
() async {
final calls = await _reconfigure(root, setUpHome: (_) {});

expect(calls, contains('apps:sdkconfig android $_appId --json'));
},
);

test(
'reconfigure fetches the service file through the Firebase CLI when the '
'configstore has no refresh token',
() async {
final calls = await _reconfigure(
root,
setUpHome: (home) {
File(
path.join(
home.path,
'.config',
'configstore',
'firebase-tools.json',
),
)
..createSync(recursive: true)
..writeAsStringSync('{}');
},
);

expect(calls, contains('apps:sdkconfig android $_appId --json'));
},
);
}
Loading