create method

Future<Map<String, dynamic>> create({
  1. required Dio apiDio,
  2. required String name,
  3. required String packageName,
})

Two calls in a fixed order — whoami before create — so the account's type/organizationId decide ownerType the way lib/ui/screens/create_app/_state.dart does, rather than the caller having to know it. FakeAdapter.calls in apps_service_test.dart proves the order. A member of an organization is refused locally, before the POST — the backend's own FORBIDDEN: 403 (appCreateService, #30) is the backstop, not the only gate, the way an already-stale whoami read could otherwise cost a wasted round trip. Individuals are never refused here — there is no "member" concept on an individual account.

Implementation

Future<Map<String, dynamic>> create({
  required Dio apiDio,
  required String name,
  required String packageName,
}) async {
  try {
    final whoamiResp = await apiDio.get(Endpoints.whoami);
    final whoami = whoamiResp.data['data'] as Map<String, dynamic>;
    final organizationId = whoami['organizationId'] as String?;
    final ownerType =
        whoami['type'] == 'organization' && organizationId != null
            ? 'organization'
            : 'individual';

    if (ownerType == 'organization' && whoami['role'] == 'member') {
      throw const AuthCliFault(
        ExitCodes.forbidden,
        'Only admins and developers can create organization apps.',
        errorCode: 'forbidden',
      );
    }

    final resp = await apiDio.post(
      Endpoints.appCreate,
      data: {
        'name': name,
        'packageName': packageName,
        'ownerType': ownerType,
        if (organizationId != null) 'organizationId': organizationId,
      },
    );
    return resp.data['data'] as Map<String, dynamic>;
  } on DioException catch (e) {
    throw _mapError(e, action: 'create app');
  }
}