Skip to main content

apktesting_reporter 0.1.0 copy "apktesting_reporter: ^0.1.0" to clipboard
apktesting_reporter: ^0.1.0 copied to clipboard

In-app bug reporting client for apktesting.com testing jobs.

example/lib/main.dart

import 'dart:io';
import 'dart:typed_data';

import 'package:apktesting_reporter/apktesting_reporter.dart';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:record/record.dart';

void main() {
  runApp(const ReporterExampleApp());
}

class ReporterExampleApp extends StatelessWidget {
  const ReporterExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'apktesting Reporter Example',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
        useMaterial3: true,
      ),
      home: const ReporterExamplePage(),
    );
  }
}

class ReporterExamplePage extends StatefulWidget {
  const ReporterExamplePage({super.key});

  @override
  State<ReporterExamplePage> createState() => _ReporterExamplePageState();
}

class _ReporterExamplePageState extends State<ReporterExamplePage> {
  final screenshotController = ApktestingScreenshotController();
  final audioRecorder = AudioRecorder();
  final titleController = TextEditingController();
  final descriptionController = TextEditingController();
  final jobId = const String.fromEnvironment('APKT_JOB_ID');
  final idToken = const String.fromEnvironment('APKT_ID_TOKEN');
  final apiBaseUrl = const String.fromEnvironment(
    'APKT_API_BASE_URL',
    defaultValue: 'http://localhost:3000',
  );
  bool isRecording = false;
  Uint8List? voiceNote;

  Future<void> toggleRecording(StateSetter setDialogState) async {
    if (isRecording) {
      final path = await audioRecorder.stop();
      if (path != null) {
        voiceNote = await File(path).readAsBytes();
      }
      isRecording = false;
      setDialogState(() {});
      return;
    }

    if (!await audioRecorder.hasPermission()) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(
            content: Text(
              'Microphone permission is required for a voice note.',
            ),
          ),
        );
      }
      return;
    }

    final directory = await getTemporaryDirectory();
    final path = '${directory.path}/apktesting-voice-note.m4a';
    await audioRecorder.start(
      const RecordConfig(encoder: AudioEncoder.aacLc),
      path: path,
    );
    isRecording = true;
    voiceNote = null;
    setDialogState(() {});
  }

  Future<void> openReporter() async {
    titleController.clear();
    descriptionController.clear();
    voiceNote = null;
    final screenshot = await screenshotController.capturePng();
    if (!mounted) return;
    await showDialog<void>(
      context: context,
      barrierDismissible: false,
      builder: (context) => StatefulBuilder(
        builder: (context, setDialogState) => AlertDialog(
          title: const Text('Report a bug'),
          content: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              TextField(
                controller: titleController,
                decoration: const InputDecoration(labelText: 'Bug title'),
              ),
              TextField(
                controller: descriptionController,
                maxLines: 4,
                decoration: const InputDecoration(labelText: 'What happened?'),
              ),
              const SizedBox(height: 12),
              OutlinedButton.icon(
                onPressed: () => toggleRecording(setDialogState),
                icon: Icon(isRecording ? Icons.stop : Icons.mic),
                label: Text(
                  isRecording ? 'Stop recording' : 'Record voice note',
                ),
              ),
              if (voiceNote != null)
                const Text(
                  'Voice note ready to upload.',
                  style: TextStyle(fontSize: 12),
                ),
            ],
          ),
          actions: [
            TextButton(
              onPressed: () async {
                if (isRecording) await audioRecorder.stop();
                isRecording = false;
                voiceNote = null;
                if (context.mounted) Navigator.pop(context);
              },
              child: const Text('Cancel'),
            ),
            FilledButton(
              onPressed: isRecording ? null : () => submitReport(screenshot),
              child: const Text('Submit'),
            ),
          ],
        ),
      ),
    );
  }

  Future<void> submitReport(Uint8List? screenshot) async {
    if (jobId.isEmpty ||
        idToken.isEmpty ||
        titleController.text.trim().isEmpty ||
        descriptionController.text.trim().isEmpty) {
      Navigator.pop(context);
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Text(
            'Configure APKT_JOB_ID, APKT_ID_TOKEN, and both report fields first.',
          ),
        ),
      );
      return;
    }

    final client = ApktestingReporterClient(
      apiBaseUrl: apiBaseUrl,
      idToken: idToken,
    );
    try {
      await client.submit(
        ApktestingReport(
          jobId: jobId,
          title: titleController.text.trim(),
          description: descriptionController.text.trim(),
          expectedResult: 'The feature works as designed.',
          actualResult: descriptionController.text.trim(),
          screenshot: screenshot,
          audio: voiceNote,
          audioFileName: 'voice-note.m4a',
        ),
      );
      if (mounted) {
        Navigator.pop(context);
        ScaffoldMessenger.of(
          context,
        ).showSnackBar(const SnackBar(content: Text('Report submitted.')));
      }
    } catch (error) {
      if (mounted) {
        Navigator.pop(context);
        ScaffoldMessenger.of(
          context,
        ).showSnackBar(SnackBar(content: Text('Report failed: $error')));
      }
    } finally {
      client.close();
    }
  }

  @override
  void dispose() {
    titleController.dispose();
    descriptionController.dispose();
    audioRecorder.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ApktestingShakeDetector(
      onShake: openReporter,
      child: screenshotController.captureSurface(
        Scaffold(
          appBar: AppBar(title: const Text('Reporter SDK example')),
          body: const Center(
            child: Text('Open the reporter button to submit a test issue.'),
          ),
          floatingActionButton: ApktestingReporterButton(
            onSubmit: openReporter,
          ),
        ),
      ),
    );
  }
}
0
likes
140
points
99
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

In-app bug reporting client for apktesting.com testing jobs.

Homepage
Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter, http, sensors_plus

More

Packages that depend on apktesting_reporter