mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-11 04:07:56 -06:00
fix: Resolve mTLS issue, update changelogs, bump version number
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
- Beheben eines Problems mit mTLS
|
||||||
|
- Kleinere Fehlerbehebungen
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
- Fix issue with mTLS
|
||||||
|
- Some minor bug fixes
|
||||||
@@ -2,13 +2,29 @@ import 'package:dio/dio.dart';
|
|||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
|
||||||
abstract class PaperlessApiFactory {
|
abstract class PaperlessApiFactory {
|
||||||
PaperlessDocumentsApi createDocumentsApi(Dio dio, {required int apiVersion});
|
PaperlessDocumentsApi createDocumentsApi(
|
||||||
PaperlessSavedViewsApi createSavedViewsApi(Dio dio,
|
Dio dio, {
|
||||||
{required int apiVersion});
|
required int apiVersion,
|
||||||
PaperlessLabelsApi createLabelsApi(Dio dio, {required int apiVersion});
|
});
|
||||||
PaperlessServerStatsApi createServerStatsApi(Dio dio,
|
PaperlessSavedViewsApi createSavedViewsApi(
|
||||||
{required int apiVersion});
|
Dio dio, {
|
||||||
PaperlessTasksApi createTasksApi(Dio dio, {required int apiVersion});
|
required int apiVersion,
|
||||||
|
});
|
||||||
|
PaperlessLabelsApi createLabelsApi(
|
||||||
|
Dio dio, {
|
||||||
|
required int apiVersion,
|
||||||
|
});
|
||||||
|
PaperlessServerStatsApi createServerStatsApi(
|
||||||
|
Dio dio, {
|
||||||
|
required int apiVersion,
|
||||||
|
});
|
||||||
|
PaperlessTasksApi createTasksApi(
|
||||||
|
Dio dio, {
|
||||||
|
required int apiVersion,
|
||||||
|
});
|
||||||
PaperlessAuthenticationApi createAuthenticationApi(Dio dio);
|
PaperlessAuthenticationApi createAuthenticationApi(Dio dio);
|
||||||
PaperlessUserApi createUserApi(Dio dio, {required int apiVersion});
|
PaperlessUserApi createUserApi(
|
||||||
|
Dio dio, {
|
||||||
|
required int apiVersion,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ class ChangelogDialog extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const _versionNumbers = {
|
const _versionNumbers = {
|
||||||
|
"4053": "3.2.1",
|
||||||
"4043": "3.2.0",
|
"4043": "3.2.0",
|
||||||
"4033": "3.1.8",
|
"4033": "3.1.8",
|
||||||
"4023": "3.1.7",
|
"4023": "3.1.7",
|
||||||
|
|||||||
@@ -8,13 +8,27 @@ part 'client_certificate.g.dart';
|
|||||||
@HiveType(typeId: HiveTypeIds.clientCertificate)
|
@HiveType(typeId: HiveTypeIds.clientCertificate)
|
||||||
class ClientCertificate {
|
class ClientCertificate {
|
||||||
@HiveField(0)
|
@HiveField(0)
|
||||||
Uint8List bytes;
|
final Uint8List bytes;
|
||||||
|
@HiveField(2, defaultValue: "cert.pfx")
|
||||||
|
final String filename;
|
||||||
@HiveField(1)
|
@HiveField(1)
|
||||||
String? passphrase;
|
final String? passphrase;
|
||||||
|
|
||||||
|
|
||||||
ClientCertificate({
|
ClientCertificate({
|
||||||
required this.bytes,
|
required this.bytes,
|
||||||
|
required this.filename,
|
||||||
this.passphrase,
|
this.passphrase,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ClientCertificate copyWith({
|
||||||
|
Uint8List? bytes,
|
||||||
|
String? filename,
|
||||||
|
String? passphrase,
|
||||||
|
}) {
|
||||||
|
return ClientCertificate(
|
||||||
|
bytes: bytes ?? this.bytes,
|
||||||
|
filename: filename ?? this.filename,
|
||||||
|
passphrase: passphrase ?? this.passphrase,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
class ClientCertificateFormModel {
|
|
||||||
static const bytesKey = 'bytes';
|
|
||||||
static const passphraseKey = 'passphrase';
|
|
||||||
|
|
||||||
final Uint8List bytes;
|
|
||||||
final String? passphrase;
|
|
||||||
|
|
||||||
ClientCertificateFormModel({
|
|
||||||
required this.bytes,
|
|
||||||
this.passphrase,
|
|
||||||
});
|
|
||||||
|
|
||||||
ClientCertificateFormModel copyWith({
|
|
||||||
Uint8List? bytes,
|
|
||||||
String? passphrase,
|
|
||||||
String? filePath,
|
|
||||||
}) {
|
|
||||||
return ClientCertificateFormModel(
|
|
||||||
bytes: bytes ?? this.bytes,
|
|
||||||
passphrase: passphrase ?? this.passphrase,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,6 @@ import 'package:paperless_mobile/core/model/info_message_exception.dart';
|
|||||||
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
|
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
|
||||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
||||||
import 'package:paperless_mobile/features/login/model/client_certificate_form_model.dart';
|
|
||||||
import 'package:paperless_mobile/features/login/model/login_form_credentials.dart';
|
import 'package:paperless_mobile/features/login/model/login_form_credentials.dart';
|
||||||
import 'package:paperless_mobile/features/login/model/reachability_status.dart';
|
import 'package:paperless_mobile/features/login/model/reachability_status.dart';
|
||||||
import 'package:paperless_mobile/features/login/view/widgets/form_fields/client_certificate_form_field.dart';
|
import 'package:paperless_mobile/features/login/view/widgets/form_fields/client_certificate_form_field.dart';
|
||||||
@@ -230,75 +229,14 @@ class _AddAccountPageState extends State<AddAccountPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: Text(widget.titleText),
|
|
||||||
),
|
|
||||||
bottomNavigationBar: BottomAppBar(
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: widget.bottomLeftButton != null
|
|
||||||
? MainAxisAlignment.spaceBetween
|
|
||||||
: MainAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
if (widget.bottomLeftButton != null) widget.bottomLeftButton!,
|
|
||||||
FilledButton(
|
|
||||||
child: Text(S.of(context)!.loginPageSignInTitle),
|
|
||||||
onPressed: _reachabilityStatus == ReachabilityStatus.reachable &&
|
|
||||||
!_isFormSubmitted
|
|
||||||
? _onSubmit
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
resizeToAvoidBottomInset: true,
|
|
||||||
body: AutofillGroup(
|
|
||||||
child: FormBuilder(
|
|
||||||
key: _formKey,
|
|
||||||
child: ListView(
|
|
||||||
children: [
|
|
||||||
ServerAddressFormField(
|
|
||||||
initialValue: widget.initialServerUrl,
|
|
||||||
onChanged: (address) {
|
|
||||||
_updateReachability(address);
|
|
||||||
},
|
|
||||||
).padded(),
|
|
||||||
ClientCertificateFormField(
|
|
||||||
initialBytes: widget.initialClientCertificate?.bytes,
|
|
||||||
initialPassphrase: widget.initialClientCertificate?.passphrase,
|
|
||||||
onChanged: (_) => _updateReachability(),
|
|
||||||
).padded(),
|
|
||||||
_buildStatusIndicator(),
|
|
||||||
if (_reachabilityStatus == ReachabilityStatus.reachable) ...[
|
|
||||||
UserCredentialsFormField(
|
|
||||||
formKey: _formKey,
|
|
||||||
initialUsername: widget.initialUsername,
|
|
||||||
initialPassword: widget.initialPassword,
|
|
||||||
onFieldsSubmitted: _onSubmit,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
S.of(context)!.loginRequiredPermissionsHint,
|
|
||||||
style: Theme.of(context).textTheme.bodySmall?.apply(
|
|
||||||
color: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.onBackground
|
|
||||||
.withOpacity(0.6),
|
|
||||||
),
|
|
||||||
).padded(16),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ReachabilityStatus> _updateReachability([String? address]) async {
|
Future<ReachabilityStatus> _updateReachability([String? address]) async {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isCheckingConnection = true;
|
_isCheckingConnection = true;
|
||||||
});
|
});
|
||||||
final certForm =
|
final selectedCertificate =
|
||||||
_formKey.currentState?.getRawValue<ClientCertificateFormModel>(
|
_formKey.currentState?.getRawValue<ClientCertificate>(
|
||||||
ClientCertificateFormField.fkClientCertificate,
|
ClientCertificateFormField.fkClientCertificate,
|
||||||
);
|
);
|
||||||
final status = await context
|
final status = await context
|
||||||
@@ -307,12 +245,7 @@ class _AddAccountPageState extends State<AddAccountPage> {
|
|||||||
address ??
|
address ??
|
||||||
_formKey.currentState!
|
_formKey.currentState!
|
||||||
.getRawValue(ServerAddressFormField.fkServerAddress),
|
.getRawValue(ServerAddressFormField.fkServerAddress),
|
||||||
certForm != null
|
selectedCertificate,
|
||||||
? ClientCertificate(
|
|
||||||
bytes: certForm.bytes,
|
|
||||||
passphrase: certForm.passphrase,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
);
|
);
|
||||||
setState(() {
|
setState(() {
|
||||||
_isCheckingConnection = false;
|
_isCheckingConnection = false;
|
||||||
@@ -383,16 +316,10 @@ class _AddAccountPageState extends State<AddAccountPage> {
|
|||||||
});
|
});
|
||||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||||
final form = _formKey.currentState!.value;
|
final form = _formKey.currentState!.value;
|
||||||
ClientCertificate? clientCert;
|
|
||||||
final clientCertFormModel =
|
final clientCertFormModel =
|
||||||
form[ClientCertificateFormField.fkClientCertificate]
|
form[ClientCertificateFormField.fkClientCertificate]
|
||||||
as ClientCertificateFormModel?;
|
as ClientCertificate?;
|
||||||
if (clientCertFormModel != null) {
|
|
||||||
clientCert = ClientCertificate(
|
|
||||||
bytes: clientCertFormModel.bytes,
|
|
||||||
passphrase: clientCertFormModel.passphrase,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
final credentials =
|
final credentials =
|
||||||
form[UserCredentialsFormField.fkCredentials] as LoginFormCredentials;
|
form[UserCredentialsFormField.fkCredentials] as LoginFormCredentials;
|
||||||
try {
|
try {
|
||||||
@@ -401,7 +328,7 @@ class _AddAccountPageState extends State<AddAccountPage> {
|
|||||||
credentials.username!,
|
credentials.username!,
|
||||||
credentials.password!,
|
credentials.password!,
|
||||||
form[ServerAddressFormField.fkServerAddress],
|
form[ServerAddressFormField.fkServerAddress],
|
||||||
clientCert,
|
clientCertFormModel,
|
||||||
);
|
);
|
||||||
} on PaperlessApiException catch (error) {
|
} on PaperlessApiException catch (error) {
|
||||||
showErrorMessage(context, error);
|
showErrorMessage(context, error);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import 'package:file_picker/file_picker.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/login/model/client_certificate_form_model.dart';
|
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
@@ -15,14 +15,16 @@ class ClientCertificateFormField extends StatefulWidget {
|
|||||||
static const fkClientCertificate = 'clientCertificate';
|
static const fkClientCertificate = 'clientCertificate';
|
||||||
|
|
||||||
final String? initialPassphrase;
|
final String? initialPassphrase;
|
||||||
|
final String? initialFilename;
|
||||||
final Uint8List? initialBytes;
|
final Uint8List? initialBytes;
|
||||||
|
|
||||||
final ValueChanged<ClientCertificateFormModel?>? onChanged;
|
final ValueChanged<ClientCertificate?>? onChanged;
|
||||||
const ClientCertificateFormField({
|
const ClientCertificateFormField({
|
||||||
super.key,
|
super.key,
|
||||||
this.onChanged,
|
this.onChanged,
|
||||||
this.initialPassphrase,
|
this.initialPassphrase,
|
||||||
this.initialBytes,
|
this.initialBytes,
|
||||||
|
this.initialFilename,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -32,23 +34,23 @@ class ClientCertificateFormField extends StatefulWidget {
|
|||||||
|
|
||||||
class _ClientCertificateFormFieldState extends State<ClientCertificateFormField>
|
class _ClientCertificateFormFieldState extends State<ClientCertificateFormField>
|
||||||
with AutomaticKeepAliveClientMixin {
|
with AutomaticKeepAliveClientMixin {
|
||||||
File? _selectedFile;
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
super.build(context);
|
super.build(context);
|
||||||
return FormBuilderField<ClientCertificateFormModel?>(
|
return FormBuilderField<ClientCertificate?>(
|
||||||
key: const ValueKey('login-client-cert'),
|
key: const ValueKey('login-client-cert'),
|
||||||
name: ClientCertificateFormField.fkClientCertificate,
|
name: ClientCertificateFormField.fkClientCertificate,
|
||||||
onChanged: widget.onChanged,
|
onChanged: widget.onChanged,
|
||||||
initialValue: widget.initialBytes != null
|
initialValue: widget.initialBytes != null
|
||||||
? ClientCertificateFormModel(
|
? ClientCertificate(
|
||||||
bytes: widget.initialBytes!,
|
bytes: widget.initialBytes!,
|
||||||
|
filename: widget.initialFilename!,
|
||||||
passphrase: widget.initialPassphrase,
|
passphrase: widget.initialPassphrase,
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
builder: (field) {
|
builder: (field) {
|
||||||
final theme =
|
final theme =
|
||||||
Theme.of(context).copyWith(dividerColor: Colors.transparent); //new
|
Theme.of(context).copyWith(dividerColor: Colors.transparent);
|
||||||
return Theme(
|
return Theme(
|
||||||
data: theme,
|
data: theme,
|
||||||
child: ExpansionTile(
|
child: ExpansionTile(
|
||||||
@@ -74,11 +76,10 @@ class _ClientCertificateFormFieldState extends State<ClientCertificateFormField>
|
|||||||
_buildSelectedFileText(field).paddedOnly(left: 8),
|
_buildSelectedFileText(field).paddedOnly(left: 8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (_selectedFile != null)
|
if (field.value?.filename != null)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.close),
|
icon: const Icon(Icons.close),
|
||||||
onPressed: () => setState(() {
|
onPressed: () => setState(() {
|
||||||
_selectedFile = null;
|
|
||||||
field.didChange(null);
|
field.didChange(null);
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -103,7 +104,7 @@ class _ClientCertificateFormFieldState extends State<ClientCertificateFormField>
|
|||||||
// : null,
|
// : null,
|
||||||
// ),
|
// ),
|
||||||
// ),
|
// ),
|
||||||
if (_selectedFile != null) ...[
|
if (field.value?.filename != null) ...[
|
||||||
ObscuredInputTextFormField(
|
ObscuredInputTextFormField(
|
||||||
key: const ValueKey('login-client-cert-passphrase'),
|
key: const ValueKey('login-client-cert-passphrase'),
|
||||||
initialValue: field.value?.passphrase,
|
initialValue: field.value?.passphrase,
|
||||||
@@ -124,7 +125,7 @@ class _ClientCertificateFormFieldState extends State<ClientCertificateFormField>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onSelectFile(
|
Future<void> _onSelectFile(
|
||||||
FormFieldState<ClientCertificateFormModel?> field,
|
FormFieldState<ClientCertificate?> field,
|
||||||
) async {
|
) async {
|
||||||
final result = await FilePicker.platform.pickFiles(
|
final result = await FilePicker.platform.pickFiles(
|
||||||
allowMultiple: false,
|
allowMultiple: false,
|
||||||
@@ -137,21 +138,18 @@ class _ClientCertificateFormFieldState extends State<ClientCertificateFormField>
|
|||||||
showSnackBar(context, S.of(context)!.invalidCertificateFormat);
|
showSnackBar(context, S.of(context)!.invalidCertificateFormat);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
File file = File(result.files.single.path!);
|
File file = File(path);
|
||||||
setState(() {
|
|
||||||
_selectedFile = file;
|
|
||||||
});
|
|
||||||
final bytes = await file.readAsBytes();
|
final bytes = await file.readAsBytes();
|
||||||
|
|
||||||
final changedValue = field.value?.copyWith(bytes: bytes) ??
|
final changedValue = ClientCertificate(
|
||||||
ClientCertificateFormModel(bytes: bytes);
|
bytes: bytes,
|
||||||
|
filename: p.basename(path),
|
||||||
|
);
|
||||||
field.didChange(changedValue);
|
field.didChange(changedValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSelectedFileText(
|
Widget _buildSelectedFileText(FormFieldState<ClientCertificate?> field) {
|
||||||
FormFieldState<ClientCertificateFormModel?> field) {
|
|
||||||
if (field.value == null) {
|
if (field.value == null) {
|
||||||
assert(_selectedFile == null);
|
|
||||||
return Text(
|
return Text(
|
||||||
S.of(context)!.selectFile,
|
S.of(context)!.selectFile,
|
||||||
style: Theme.of(context).textTheme.labelMedium?.apply(
|
style: Theme.of(context).textTheme.labelMedium?.apply(
|
||||||
@@ -159,9 +157,8 @@ class _ClientCertificateFormFieldState extends State<ClientCertificateFormField>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
assert(_selectedFile != null);
|
|
||||||
return Text(
|
return Text(
|
||||||
_selectedFile!.path.split("/").last,
|
p.basename(field.value!.filename),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ part 'custom_field_model.g.dart';
|
|||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class CustomFieldModel with EquatableMixin {
|
class CustomFieldModel with EquatableMixin {
|
||||||
final int? id;
|
final int? id;
|
||||||
final String name;
|
final String? name;
|
||||||
final CustomFieldDataType dataType;
|
final CustomFieldDataType dataType;
|
||||||
|
|
||||||
CustomFieldModel({
|
CustomFieldModel({
|
||||||
@@ -24,3 +24,18 @@ class CustomFieldModel with EquatableMixin {
|
|||||||
|
|
||||||
Map<String, dynamic> toJson() => _$CustomFieldModelToJson(this);
|
Map<String, dynamic> toJson() => _$CustomFieldModelToJson(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An instance of the [CustomFieldModel].
|
||||||
|
@JsonSerializable()
|
||||||
|
class CustomFieldInstance {
|
||||||
|
final int? id;
|
||||||
|
final dynamic value;
|
||||||
|
|
||||||
|
const CustomFieldInstance({
|
||||||
|
this.id,
|
||||||
|
this.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory CustomFieldInstance.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$CustomFieldInstanceFromJson(json);
|
||||||
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class DocumentModel extends Equatable {
|
|||||||
|
|
||||||
/// Only present if full_perms=true
|
/// Only present if full_perms=true
|
||||||
final Permissions? permissions;
|
final Permissions? permissions;
|
||||||
final Iterable<CustomFieldModel> customFields;
|
final Iterable<CustomFieldInstance> customFields;
|
||||||
|
|
||||||
const DocumentModel({
|
const DocumentModel({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -98,7 +98,7 @@ class DocumentModel extends Equatable {
|
|||||||
bool? userCanChange,
|
bool? userCanChange,
|
||||||
Iterable<NoteModel>? notes,
|
Iterable<NoteModel>? notes,
|
||||||
Permissions? permissions,
|
Permissions? permissions,
|
||||||
Iterable<CustomFieldModel>? customFields,
|
Iterable<CustomFieldInstance>? customFields,
|
||||||
}) {
|
}) {
|
||||||
return DocumentModel(
|
return DocumentModel(
|
||||||
id: id,
|
id: id,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
version: 3.2.0+404
|
version: 3.2.1+405
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.1.0 <4.0.0"
|
sdk: ">=3.1.0 <4.0.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user